From 814d0b1e3953bb865e9bdb1e079f5cb246994fcc Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 19 Dec 2019 10:20:46 -0600 Subject: [PATCH] Add support for additional Vault authentication methods. (#1526) * Add support for additional Vault authentication methods. * Fix Vault client config test setup so it doesn't rely on local state. Fixes gh-1475 --- pom.xml | 6 + spring-cloud-config-server/pom.xml | 5 + .../EnvironmentRepositoryConfiguration.java | 12 +- .../VaultEnvironmentProperties.java | 713 ++++++++++++++++++ .../vault/SpringVaultClientConfiguration.java | 582 ++++++++++++++ ...ringVaultEnvironmentRepositoryFactory.java | 67 +- .../SpringVaultClientConfigurationTests.java | 395 ++++++++++ ...aultEnvironmentRepositoryFactoryTests.java | 26 +- 8 files changed, 1732 insertions(+), 74 deletions(-) create mode 100644 spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfiguration.java create mode 100644 spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfigurationTests.java diff --git a/pom.xml b/pom.xml index 8492d500..281e0f58 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ config 2.2.1.BUILD-SNAPSHOT 1.11.52 + v1-rev20191010-1.30.3 true true @@ -76,6 +77,11 @@ aws-java-sdk-s3 ${aws-java-sdk.version} + + com.google.apis + google-api-services-iam + ${google-api-services-iam.version} + com.google.auth google-auth-library-oauth2-http diff --git a/spring-cloud-config-server/pom.xml b/spring-cloud-config-server/pom.xml index 62580b77..35fc4686 100644 --- a/spring-cloud-config-server/pom.xml +++ b/spring-cloud-config-server/pom.xml @@ -99,6 +99,11 @@ spring-boot-autoconfigure-processor true + + com.google.apis + google-api-services-iam + true + com.google.auth google-auth-library-oauth2-http diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java index 29f61970..5dfdfddb 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/config/EnvironmentRepositoryConfiguration.java @@ -71,6 +71,7 @@ import org.springframework.cloud.config.server.environment.SvnKitEnvironmentRepo import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties; import org.springframework.cloud.config.server.environment.VaultEnvironmentRepository; import org.springframework.cloud.config.server.environment.VaultEnvironmentRepositoryFactory; +import org.springframework.cloud.config.server.environment.vault.SpringVaultClientConfiguration; import org.springframework.cloud.config.server.environment.vault.SpringVaultEnvironmentRepository; import org.springframework.cloud.config.server.environment.vault.SpringVaultEnvironmentRepositoryFactory; import org.springframework.cloud.config.server.support.GoogleCloudSourceSupport; @@ -244,12 +245,19 @@ public class EnvironmentRepositoryConfiguration { @ConditionalOnClass(VaultTemplate.class) static class SpringVaultFactoryConfig { + @Bean + public SpringVaultClientConfiguration vaultClientConfiguration( + VaultEnvironmentProperties vaultProperties, + ConfigTokenProvider tokenProvider) { + return new SpringVaultClientConfiguration(vaultProperties, tokenProvider); + } + @Bean public SpringVaultEnvironmentRepositoryFactory vaultEnvironmentRepositoryFactory( ObjectProvider request, EnvironmentWatch watch, - ConfigTokenProvider tokenProvider) { + SpringVaultClientConfiguration vaultClientConfiguration) { return new SpringVaultEnvironmentRepositoryFactory(request, watch, - tokenProvider); + vaultClientConfiguration); } } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java index 97c77320..f561beb2 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/VaultEnvironmentProperties.java @@ -16,17 +16,24 @@ package org.springframework.cloud.config.server.environment; +import java.net.URI; +import java.time.Duration; import java.util.HashMap; import java.util.Map; +import javax.validation.constraints.NotEmpty; + import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.config.server.proxy.ProxyHostProperties; import org.springframework.cloud.config.server.support.HttpEnvironmentRepositoryProperties; import org.springframework.core.Ordered; +import org.springframework.core.io.Resource; +import org.springframework.validation.annotation.Validated; /** * @author Dylan Roberts * @author Haroun Pacquee + * @author Scott Frederick */ @ConfigurationProperties("spring.cloud.config.server.vault") public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProperties { @@ -79,6 +86,31 @@ public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProp */ private String namespace; + /** + * Static vault token. Required if {@link #authentication} is {@code TOKEN}. + */ + private String token; + + private AppRoleProperties appRole = new AppRoleProperties(); + + private AwsEc2Properties awsEc2 = new AwsEc2Properties(); + + private AwsIamProperties awsIam = new AwsIamProperties(); + + private AzureMsiProperties azureMsi = new AzureMsiProperties(); + + private GcpGceProperties gcpGce = new GcpGceProperties(); + + private GcpIamProperties gcpIam = new GcpIamProperties(); + + private KubernetesProperties kubernetes = new KubernetesProperties(); + + private PcfProperties pcf = new PcfProperties(); + + private Ssl ssl = new Ssl(); + + private AuthenticationMethod authentication; + public String getHost() { return this.host; } @@ -180,4 +212,685 @@ public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProp this.namespace = namespace; } + public String getToken() { + return this.token; + } + + public void setToken(String token) { + this.token = token; + } + + public AppRoleProperties getAppRole() { + return this.appRole; + } + + public AwsEc2Properties getAwsEc2() { + return this.awsEc2; + } + + public AwsIamProperties getAwsIam() { + return this.awsIam; + } + + public AzureMsiProperties getAzureMsi() { + return this.azureMsi; + } + + public GcpGceProperties getGcpGce() { + return this.gcpGce; + } + + public GcpIamProperties getGcpIam() { + return this.gcpIam; + } + + public KubernetesProperties getKubernetes() { + return this.kubernetes; + } + + public PcfProperties getPcf() { + return this.pcf; + } + + public Ssl getSsl() { + return this.ssl; + } + + public void setAuthentication(AuthenticationMethod authentication) { + this.authentication = authentication; + } + + public AuthenticationMethod getAuthentication() { + return authentication; + } + + public enum AuthenticationMethod { + + /** + * Vault AppRole machine authentication. + */ + APPROLE, + + /** + * Amazon Web Services Compute authentication. + */ + AWS_EC2, + + /** + * Amazon Web Services IAM authentication. + */ + AWS_IAM, + + /** + * Azure Cloud MSI authentication. + */ + AZURE_MSI, + + /** + * TLS certificate authentication. + */ + CERT, + + /** + * Cubbyhole token authentication. + */ + CUBBYHOLE, + + /** + * Google Cloud Compute authentication. + */ + GCP_GCE, + + /** + * Google Cloud IAM authentication. + */ + GCP_IAM, + + /** + * Kubernetes service account token authentication. + */ + KUBERNETES, + + /** + * Cloud Foundry instance identity certificate authentication. + */ + PCF, + + /** + * Static token authentication. + */ + TOKEN + + } + + /** + * AppRole properties. + */ + @Validated + public static class AppRoleProperties { + + /** + * Mount path of the AppRole authentication backend. + */ + private String appRolePath = "approle"; + + /** + * Name of the role, optional, used for pull-mode. + */ + private String role = ""; + + /** + * The RoleId. + */ + private String roleId = null; + + /** + * The SecretId. + */ + private String secretId = null; + + public String getAppRolePath() { + return this.appRolePath; + } + + public String getRole() { + return this.role; + } + + public String getRoleId() { + return this.roleId; + } + + public String getSecretId() { + return this.secretId; + } + + public void setAppRolePath(String appRolePath) { + this.appRolePath = appRolePath; + } + + public void setRole(String role) { + this.role = role; + } + + public void setRoleId(String roleId) { + this.roleId = roleId; + } + + public void setSecretId(String secretId) { + this.secretId = secretId; + } + + } + + /** + * AWS-EC2 properties. + */ + @Validated + public static class AwsEc2Properties { + + /** + * URL of the AWS-EC2 PKCS7 identity document. + */ + @NotEmpty + private String identityDocument = "http://169.254.169.254/latest/dynamic/instance-identity/pkcs7"; + + /** + * Mount path of the AWS-EC2 authentication backend. + */ + @NotEmpty + private String awsEc2Path = "aws-ec2"; + + /** + * Name of the role, optional. + */ + private String role = ""; + + /** + * Nonce used for AWS-EC2 authentication. An empty nonce defaults to nonce + * generation. + */ + private String nonce; + + public String getIdentityDocument() { + return this.identityDocument; + } + + public String getAwsEc2Path() { + return this.awsEc2Path; + } + + public String getRole() { + return this.role; + } + + public String getNonce() { + return this.nonce; + } + + public void setIdentityDocument(String identityDocument) { + this.identityDocument = identityDocument; + } + + public void setAwsEc2Path(String awsEc2Path) { + this.awsEc2Path = awsEc2Path; + } + + public void setRole(String role) { + this.role = role; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + } + + /** + * AWS-IAM properties. + */ + public static class AwsIamProperties { + + /** + * Mount path of the AWS authentication backend. + */ + @NotEmpty + private String awsPath = "aws"; + + /** + * Name of the role, optional. Defaults to the friendly IAM name if not set. + */ + private String role = ""; + + /** + * Name of the server used to set {@code X-Vault-AWS-IAM-Server-ID} header in the + * headers of login requests. + */ + private String serverName; + + /** + * STS server URI. + * + * @since 2.2 + */ + private URI endpointUri; + + public String getAwsPath() { + return this.awsPath; + } + + public String getRole() { + return this.role; + } + + public String getServerName() { + return this.serverName; + } + + public void setAwsPath(String awsPath) { + this.awsPath = awsPath; + } + + public void setRole(String role) { + this.role = role; + } + + public void setServerName(String serverName) { + this.serverName = serverName; + } + + public URI getEndpointUri() { + return this.endpointUri; + } + + public void setEndpointUri(URI endpointUri) { + this.endpointUri = endpointUri; + } + + } + + /** + * Azure MSI properties. + */ + public static class AzureMsiProperties { + + /** + * Mount path of the Azure MSI authentication backend. + */ + @NotEmpty + private String azurePath = "azure"; + + /** + * Name of the role. + */ + private String role = ""; + + public String getAzurePath() { + return this.azurePath; + } + + public String getRole() { + return this.role; + } + + public void setAzurePath(String azurePath) { + this.azurePath = azurePath; + } + + public void setRole(String role) { + this.role = role; + } + + } + + /** + * GCP-GCE properties. + */ + public static class GcpGceProperties { + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String gcpPath = "gcp"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Optional service account id. Using the default id if left unconfigured. + */ + private String serviceAccount = ""; + + public String getGcpPath() { + return this.gcpPath; + } + + public String getRole() { + return this.role; + } + + public String getServiceAccount() { + return this.serviceAccount; + } + + public void setGcpPath(String gcpPath) { + this.gcpPath = gcpPath; + } + + public void setRole(String role) { + this.role = role; + } + + public void setServiceAccount(String serviceAccount) { + this.serviceAccount = serviceAccount; + } + + } + + /** + * GCP-IAM properties. + */ + public static class GcpIamProperties { + + /** + * Credentials configuration. + */ + private final GcpCredentials credentials = new GcpCredentials(); + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String gcpPath = "gcp"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Overrides the GCP project Id. + */ + private String projectId = ""; + + /** + * Overrides the GCP service account Id. + */ + private String serviceAccountId = ""; + + /** + * Validity of the JWT token. + */ + private Duration jwtValidity = Duration.ofMinutes(15); + + public GcpCredentials getCredentials() { + return this.credentials; + } + + public String getGcpPath() { + return this.gcpPath; + } + + public String getRole() { + return this.role; + } + + public String getProjectId() { + return this.projectId; + } + + public String getServiceAccountId() { + return this.serviceAccountId; + } + + public Duration getJwtValidity() { + return this.jwtValidity; + } + + public void setGcpPath(String gcpPath) { + this.gcpPath = gcpPath; + } + + public void setRole(String role) { + this.role = role; + } + + public void setProjectId(String projectId) { + this.projectId = projectId; + } + + public void setServiceAccountId(String serviceAccountId) { + this.serviceAccountId = serviceAccountId; + } + + public void setJwtValidity(Duration jwtValidity) { + this.jwtValidity = jwtValidity; + } + + } + + /** + * GCP credential properties. + */ + public static class GcpCredentials { + + /** + * Location of the OAuth2 credentials private key. + * + *

+ * Since this is a Resource, the private key can be in a multitude of locations, + * such as a local file system, classpath, URL, etc. + */ + private Resource location; + + /** + * The base64 encoded contents of an OAuth2 account private key in JSON format. + */ + private String encodedKey; + + public Resource getLocation() { + return this.location; + } + + public String getEncodedKey() { + return this.encodedKey; + } + + public void setLocation(Resource location) { + this.location = location; + } + + public void setEncodedKey(String encodedKey) { + this.encodedKey = encodedKey; + } + + } + + /** + * Kubernetes properties. + */ + public static class KubernetesProperties { + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String kubernetesPath = "kubernetes"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Path to the service account token file. + */ + @NotEmpty + private String serviceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + + public String getKubernetesPath() { + return this.kubernetesPath; + } + + public String getRole() { + return this.role; + } + + public String getServiceAccountTokenFile() { + return this.serviceAccountTokenFile; + } + + public void setKubernetesPath(String kubernetesPath) { + this.kubernetesPath = kubernetesPath; + } + + public void setRole(String role) { + this.role = role; + } + + public void setServiceAccountTokenFile(String serviceAccountTokenFile) { + this.serviceAccountTokenFile = serviceAccountTokenFile; + } + + } + + /** + * PCF properties. + */ + public static class PcfProperties { + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String pcfPath = "pcf"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Path to the instance certificate (PEM). Defaults to {@code CF_INSTANCE_CERT} + * env variable. + */ + private Resource instanceCertificate; + + /** + * Path to the instance key (PEM). Defaults to {@code CF_INSTANCE_KEY} env + * variable. + */ + private Resource instanceKey; + + public String getPcfPath() { + return this.pcfPath; + } + + public void setPcfPath(String pcfPath) { + this.pcfPath = pcfPath; + } + + public String getRole() { + return this.role; + } + + public void setRole(String role) { + this.role = role; + } + + public Resource getInstanceCertificate() { + return this.instanceCertificate; + } + + public void setInstanceCertificate(Resource instanceCertificate) { + this.instanceCertificate = instanceCertificate; + } + + public Resource getInstanceKey() { + return this.instanceKey; + } + + public void setInstanceKey(Resource instanceKey) { + this.instanceKey = instanceKey; + } + + } + + /** + * SSL properties. + */ + @Validated + public static class Ssl { + + /** + * Trust store that holds certificates and private keys. + */ + private Resource keyStore; + + /** + * Password used to access the key store. + */ + private String keyStorePassword; + + /** + * Trust store that holds SSL certificates. + */ + private Resource trustStore; + + /** + * Password used to access the trust store. + */ + private String trustStorePassword; + + /** + * Mount path of the TLS cert authentication backend. + */ + @NotEmpty + private String certAuthPath = "cert"; + + public Resource getKeyStore() { + return this.keyStore; + } + + public String getKeyStorePassword() { + return this.keyStorePassword; + } + + public Resource getTrustStore() { + return this.trustStore; + } + + public String getTrustStorePassword() { + return this.trustStorePassword; + } + + public String getCertAuthPath() { + return this.certAuthPath; + } + + public void setKeyStore(Resource keyStore) { + this.keyStore = keyStore; + } + + public void setKeyStorePassword(String keyStorePassword) { + this.keyStorePassword = keyStorePassword; + } + + public void setTrustStore(Resource trustStore) { + this.trustStore = trustStore; + } + + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + public void setCertAuthPath(String certAuthPath) { + this.certAuthPath = certAuthPath; + } + + } + } diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfiguration.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfiguration.java new file mode 100644 index 00000000..7ea39bce --- /dev/null +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfiguration.java @@ -0,0 +1,582 @@ +/* + * Copyright 2018-2019 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.config.server.environment.vault; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicReference; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.config.server.environment.ConfigTokenProvider; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AppRoleProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AwsEc2Properties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AwsIamProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AzureMsiProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.GcpCredentials; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.GcpIamProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.KubernetesProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.PcfProperties; +import org.springframework.core.io.Resource; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AppRoleAuthentication; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.AppRoleAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; +import org.springframework.vault.authentication.AwsEc2Authentication; +import org.springframework.vault.authentication.AwsEc2AuthenticationOptions; +import org.springframework.vault.authentication.AwsEc2AuthenticationOptions.Nonce; +import org.springframework.vault.authentication.AwsIamAuthentication; +import org.springframework.vault.authentication.AwsIamAuthenticationOptions; +import org.springframework.vault.authentication.AwsIamAuthenticationOptions.AwsIamAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.AzureMsiAuthentication; +import org.springframework.vault.authentication.AzureMsiAuthenticationOptions; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.ClientCertificateAuthentication; +import org.springframework.vault.authentication.CubbyholeAuthentication; +import org.springframework.vault.authentication.CubbyholeAuthenticationOptions; +import org.springframework.vault.authentication.GcpComputeAuthentication; +import org.springframework.vault.authentication.GcpComputeAuthenticationOptions; +import org.springframework.vault.authentication.GcpComputeAuthenticationOptions.GcpComputeAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.GcpCredentialSupplier; +import org.springframework.vault.authentication.GcpIamAuthentication; +import org.springframework.vault.authentication.GcpIamAuthenticationOptions; +import org.springframework.vault.authentication.GcpIamAuthenticationOptions.GcpIamAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.KubernetesAuthentication; +import org.springframework.vault.authentication.KubernetesAuthenticationOptions; +import org.springframework.vault.authentication.KubernetesServiceAccountTokenFile; +import org.springframework.vault.authentication.PcfAuthentication; +import org.springframework.vault.authentication.PcfAuthenticationOptions; +import org.springframework.vault.authentication.PcfAuthenticationOptions.PcfAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.ResourceCredentialSupplier; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.RestTemplateBuilder; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultEndpointProvider; +import org.springframework.vault.config.AbstractVaultConfiguration; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.APPROLE; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.AWS_IAM; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.AZURE_MSI; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.CUBBYHOLE; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.GCP_GCE; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.GCP_IAM; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.KUBERNETES; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.PCF; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.TOKEN; + +/** + * This class is adapted from + * {@link org.springframework.vault.config.EnvironmentVaultConfiguration} and + * org.springframework.cloud.vault.config.ClientAuthenticationFactory in order to + * provide configuration consistent with Spring Cloud Vault's property-based + * configuration. + * + * @author Scott Frederick + */ +public class SpringVaultClientConfiguration extends AbstractVaultConfiguration { + + private static final String VAULT_PROPERTIES_PREFIX = "spring.cloud.config.server.vault."; + + private final VaultEnvironmentProperties vaultProperties; + + private final ConfigTokenProvider configTokenProvider; + + private final RestOperations externalRestOperations; + + private final Log log = LogFactory.getLog(getClass()); + + public SpringVaultClientConfiguration(VaultEnvironmentProperties vaultProperties, + ConfigTokenProvider configTokenProvider) { + this.vaultProperties = vaultProperties; + this.configTokenProvider = configTokenProvider; + this.externalRestOperations = new RestTemplate( + clientHttpRequestFactoryWrapper().getClientHttpRequestFactory()); + } + + @Override + public VaultEndpoint vaultEndpoint() { + + URI baseUrl = UriComponentsBuilder.newInstance() + .scheme(vaultProperties.getScheme()).host(vaultProperties.getHost()) + .port(vaultProperties.getPort()).build().toUri(); + + return VaultEndpoint.from(baseUrl); + } + + @Override + protected RestTemplateBuilder restTemplateBuilder( + VaultEndpointProvider endpointProvider, + ClientHttpRequestFactory requestFactory) { + + RestTemplateBuilder restTemplateBuilder = super.restTemplateBuilder( + endpointProvider, requestFactory); + + if (vaultProperties.getNamespace() != null) { + restTemplateBuilder.customizers( + restTemplate -> restTemplate.getInterceptors().add(VaultClients + .createNamespaceInterceptor(vaultProperties.getNamespace()))); + } + + return restTemplateBuilder; + } + + @Override + public SslConfiguration sslConfiguration() { + if (vaultProperties.isSkipSslValidation()) { + log.warn("The '" + VAULT_PROPERTIES_PREFIX + "skipSslValidation' property " + + "is not supported by this Vault environment repository implementation. " + + "Use the '" + VAULT_PROPERTIES_PREFIX + + "ssl` properties to provide " + + "custom keyStore and trustStore material instead."); + } + + VaultEnvironmentProperties.Ssl ssl = vaultProperties.getSsl(); + + SslConfiguration.KeyStoreConfiguration keyStoreConfiguration = getKeyStoreConfiguration( + ssl.getKeyStore(), ssl.getKeyStorePassword()); + + SslConfiguration.KeyStoreConfiguration trustStoreConfiguration = getKeyStoreConfiguration( + ssl.getTrustStore(), ssl.getTrustStorePassword()); + + return new SslConfiguration(keyStoreConfiguration, trustStoreConfiguration); + } + + private SslConfiguration.KeyStoreConfiguration getKeyStoreConfiguration( + Resource resourceProperty, String passwordProperty) { + + if (resourceProperty == null) { + return SslConfiguration.KeyStoreConfiguration.unconfigured(); + } + + if (StringUtils.hasText(passwordProperty)) { + return SslConfiguration.KeyStoreConfiguration.of(resourceProperty, + passwordProperty.toCharArray()); + } + + return SslConfiguration.KeyStoreConfiguration.of(resourceProperty); + } + + /** + * @return a new {@link ClientAuthentication}. + */ + public ClientAuthentication clientAuthentication() { + + AuthenticationMethod authentication = this.vaultProperties.getAuthentication(); + + if (authentication == null) { + return new ConfigTokenProviderAuthentication(configTokenProvider); + } + + switch (authentication) { + case APPROLE: + return appRoleAuthentication(this.vaultProperties); + case AWS_EC2: + return awsEc2Authentication(this.vaultProperties); + case AWS_IAM: + return awsIamAuthentication(this.vaultProperties); + case AZURE_MSI: + return azureMsiAuthentication(this.vaultProperties); + case CERT: + return new ClientCertificateAuthentication(restOperations()); + case CUBBYHOLE: + return cubbyholeAuthentication(); + case GCP_GCE: + return gcpGceAuthentication(this.vaultProperties); + case GCP_IAM: + return gcpIamAuthentication(this.vaultProperties); + case KUBERNETES: + return kubernetesAuthentication(this.vaultProperties); + case PCF: + return pcfAuthentication(this.vaultProperties); + case TOKEN: + Assert.hasText(this.vaultProperties.getToken(), + missingPropertyForAuthMethod("token", TOKEN)); + return new TokenAuthentication(this.vaultProperties.getToken()); + } + + throw new UnsupportedOperationException( + String.format("Client authentication %s not supported", authentication)); + } + + private ClientAuthentication appRoleAuthentication( + VaultEnvironmentProperties vaultProperties) { + + AppRoleAuthenticationOptions options = getAppRoleAuthenticationOptions( + vaultProperties); + + return new AppRoleAuthentication(options, restOperations()); + } + + static AppRoleAuthenticationOptions getAppRoleAuthenticationOptions( + VaultEnvironmentProperties vaultProperties) { + + AppRoleProperties appRole = vaultProperties.getAppRole(); + + AppRoleAuthenticationOptionsBuilder builder = AppRoleAuthenticationOptions + .builder().path(appRole.getAppRolePath()); + + if (StringUtils.hasText(appRole.getRole())) { + builder.appRole(appRole.getRole()); + } + + RoleId roleId = getRoleId(vaultProperties, appRole); + SecretId secretId = getSecretId(vaultProperties, appRole); + + builder.roleId(roleId).secretId(secretId); + + return builder.build(); + } + + private static RoleId getRoleId(VaultEnvironmentProperties vaultProperties, + AppRoleProperties appRole) { + + if (StringUtils.hasText(appRole.getRoleId())) { + return RoleId.provided(appRole.getRoleId()); + } + + if (StringUtils.hasText(vaultProperties.getToken()) + && StringUtils.hasText(appRole.getRole())) { + return RoleId.pull(VaultToken.of(vaultProperties.getToken())); + } + + if (StringUtils.hasText(vaultProperties.getToken())) { + return RoleId.wrapped(VaultToken.of(vaultProperties.getToken())); + } + + throw new IllegalArgumentException( + "Any of '" + VAULT_PROPERTIES_PREFIX + "app-role.role-id', '.token', " + + "or '.app-role.role' and '.token' must be provided if the " + + APPROLE + " authentication method is specified."); + } + + private static SecretId getSecretId(VaultEnvironmentProperties vaultProperties, + AppRoleProperties appRole) { + + if (StringUtils.hasText(appRole.getSecretId())) { + return SecretId.provided(appRole.getSecretId()); + } + + if (StringUtils.hasText(vaultProperties.getToken()) + && StringUtils.hasText(appRole.getRole())) { + return SecretId.pull(VaultToken.of(vaultProperties.getToken())); + } + + if (StringUtils.hasText(vaultProperties.getToken())) { + return SecretId.wrapped(VaultToken.of(vaultProperties.getToken())); + } + + return SecretId.absent(); + } + + private ClientAuthentication awsEc2Authentication( + VaultEnvironmentProperties vaultProperties) { + + AwsEc2Properties awsEc2 = vaultProperties.getAwsEc2(); + + Nonce nonce = StringUtils.hasText(awsEc2.getNonce()) + ? Nonce.provided(awsEc2.getNonce().toCharArray()) : Nonce.generated(); + + AwsEc2AuthenticationOptions authenticationOptions = AwsEc2AuthenticationOptions + .builder().role(awsEc2.getRole()) // + .path(awsEc2.getAwsEc2Path()) // + .nonce(nonce) // + .identityDocumentUri(URI.create(awsEc2.getIdentityDocument())) // + .build(); + + return new AwsEc2Authentication(authenticationOptions, restOperations(), + this.externalRestOperations); + } + + private ClientAuthentication awsIamAuthentication( + VaultEnvironmentProperties vaultProperties) { + + assertClassPresent("com.amazonaws.auth.AWSCredentials", missingClassForAuthMethod( + "AWSCredentials", "aws-java-sdk-core", AWS_IAM)); + + AwsIamProperties awsIam = vaultProperties.getAwsIam(); + + AWSCredentialsProvider credentialsProvider = AwsCredentialProvider + .getAwsCredentialsProvider(); + + AwsIamAuthenticationOptionsBuilder builder = AwsIamAuthenticationOptions + .builder(); + + if (StringUtils.hasText(awsIam.getRole())) { + builder.role(awsIam.getRole()); + } + + if (StringUtils.hasText(awsIam.getServerName())) { + builder.serverName(awsIam.getServerName()); + } + + if (awsIam.getEndpointUri() != null) { + builder.endpointUri(awsIam.getEndpointUri()); + } + + builder.path(awsIam.getAwsPath()) // + .credentialsProvider(credentialsProvider); + + AwsIamAuthenticationOptions options = builder + .credentialsProvider(credentialsProvider).build(); + + return new AwsIamAuthentication(options, restOperations()); + } + + private ClientAuthentication azureMsiAuthentication( + VaultEnvironmentProperties vaultProperties) { + + AzureMsiProperties azureMsi = vaultProperties.getAzureMsi(); + + Assert.hasText(azureMsi.getRole(), + missingPropertyForAuthMethod("azure-msi.role", AZURE_MSI)); + + AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder() + .role(azureMsi.getRole()).build(); + + return new AzureMsiAuthentication(options, restOperations(), + this.externalRestOperations); + } + + private ClientAuthentication cubbyholeAuthentication() { + + String token = this.vaultProperties.getToken(); + + Assert.hasText(token, missingPropertyForAuthMethod("token", CUBBYHOLE)); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() // + .wrapped() // + .initialToken(VaultToken.of(token)) // + .build(); + + return new CubbyholeAuthentication(options, restOperations()); + } + + private ClientAuthentication gcpGceAuthentication( + VaultEnvironmentProperties vaultProperties) { + + VaultEnvironmentProperties.GcpGceProperties gcp = vaultProperties.getGcpGce(); + + Assert.hasText(gcp.getRole(), + missingPropertyForAuthMethod("gcp-iam.role", GCP_GCE)); + + GcpComputeAuthenticationOptionsBuilder builder = GcpComputeAuthenticationOptions + .builder().path(gcp.getGcpPath()).role(gcp.getRole()); + + if (StringUtils.hasText(gcp.getServiceAccount())) { + builder.serviceAccount(gcp.getServiceAccount()); + } + + return new GcpComputeAuthentication(builder.build(), restOperations(), + this.externalRestOperations); + } + + private ClientAuthentication gcpIamAuthentication( + VaultEnvironmentProperties vaultProperties) { + + assertClassPresent( + "com.google.api.client.googleapis.auth.oauth2.GoogleCredential", + missingClassForAuthMethod("GoogleCredential", "google-api-client", + GCP_IAM)); + + VaultEnvironmentProperties.GcpIamProperties gcp = vaultProperties.getGcpIam(); + + Assert.hasText(gcp.getRole(), + missingPropertyForAuthMethod("gcp-iam.role", GCP_IAM)); + + GcpIamAuthenticationOptionsBuilder builder = GcpIamAuthenticationOptions.builder() + .path(gcp.getGcpPath()).role(gcp.getRole()) + .jwtValidity(gcp.getJwtValidity()); + + if (StringUtils.hasText(gcp.getProjectId())) { + builder.projectId(gcp.getProjectId()); + } + + if (StringUtils.hasText(gcp.getServiceAccountId())) { + builder.serviceAccountId(gcp.getServiceAccountId()); + } + + GcpCredentialSupplier supplier = GcpCredentialProvider.getGoogleCredential(gcp); + builder.credential(supplier.get()); + + GcpIamAuthenticationOptions options = builder.build(); + + return new GcpIamAuthentication(options, restOperations()); + } + + private ClientAuthentication kubernetesAuthentication( + VaultEnvironmentProperties vaultProperties) { + + KubernetesProperties kubernetes = vaultProperties.getKubernetes(); + + Assert.hasText(kubernetes.getRole(), + missingPropertyForAuthMethod("kubernetes.role", KUBERNETES)); + Assert.hasText(kubernetes.getServiceAccountTokenFile(), + missingPropertyForAuthMethod("kubernetes.service-account-token-file", + KUBERNETES)); + + KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions + .builder().path(kubernetes.getKubernetesPath()).role(kubernetes.getRole()) + .jwtSupplier(new KubernetesServiceAccountTokenFile( + kubernetes.getServiceAccountTokenFile())) + .build(); + + return new KubernetesAuthentication(options, restOperations()); + } + + private ClientAuthentication pcfAuthentication( + VaultEnvironmentProperties vaultProperties) { + + PcfProperties pcfProperties = vaultProperties.getPcf(); + + assertClassPresent("org.bouncycastle.crypto.signers.PSSSigner", + missingClassForAuthMethod("BouncyCastle", "bcpkix-jdk15on", PCF)); + Assert.hasText(pcfProperties.getRole(), + missingPropertyForAuthMethod("pcf.role", PCF)); + + PcfAuthenticationOptionsBuilder builder = PcfAuthenticationOptions.builder() + .role(pcfProperties.getRole()).path(pcfProperties.getPcfPath()); + + if (pcfProperties.getInstanceCertificate() != null) { + builder.instanceCertificate(new ResourceCredentialSupplier( + pcfProperties.getInstanceCertificate())); + } + + if (pcfProperties.getInstanceKey() != null) { + builder.instanceKey( + new ResourceCredentialSupplier(pcfProperties.getInstanceKey())); + } + + return new PcfAuthentication(builder.build(), restOperations()); + } + + private String missingPropertyForAuthMethod(String propertyName, + AuthenticationMethod authenticationMethod) { + return "The '" + VAULT_PROPERTIES_PREFIX + propertyName + + "' property must be provided " + "when the " + authenticationMethod + + " authentication method is specified."; + } + + private String missingClassForAuthMethod(String className, String classArtifact, + AuthenticationMethod authenticationMethod) { + return className + "(" + classArtifact + ")" + + " must be on the classpath when the " + authenticationMethod + + " authentication method is specified"; + } + + private void assertClassPresent(String className, String message) { + Assert.isTrue(ClassUtils.isPresent(className, getClass().getClassLoader()), + message); + } + + private static class AwsCredentialProvider { + + private static AWSCredentialsProvider getAwsCredentialsProvider() { + + DefaultAWSCredentialsProviderChain backingCredentialsProvider = DefaultAWSCredentialsProviderChain + .getInstance(); + + // Eagerly fetch credentials preventing lag during the first, actual login. + AWSCredentials firstAccess = backingCredentialsProvider.getCredentials(); + + AtomicReference once = new AtomicReference<>(firstAccess); + + return new AWSCredentialsProvider() { + + @Override + public AWSCredentials getCredentials() { + + if (once.compareAndSet(firstAccess, null)) { + return firstAccess; + } + + return backingCredentialsProvider.getCredentials(); + } + + @Override + public void refresh() { + backingCredentialsProvider.refresh(); + } + }; + } + + } + + @SuppressWarnings("deprecation") + private static class GcpCredentialProvider { + + public static GcpCredentialSupplier getGoogleCredential(GcpIamProperties gcp) { + return () -> { + + GcpCredentials credentialProperties = gcp.getCredentials(); + if (credentialProperties.getLocation() != null) { + return GoogleCredential.fromStream( + credentialProperties.getLocation().getInputStream()); + } + + if (StringUtils.hasText(credentialProperties.getEncodedKey())) { + return GoogleCredential.fromStream(new ByteArrayInputStream(Base64 + .getDecoder().decode(credentialProperties.getEncodedKey()))); + } + + return GoogleCredential.getApplicationDefault(); + }; + } + + } + + static class ConfigTokenProviderAuthentication implements ClientAuthentication { + + private final ConfigTokenProvider tokenProvider; + + ConfigTokenProviderAuthentication(ConfigTokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public VaultToken login() throws VaultException { + String token = tokenProvider.getToken(); + if (!StringUtils.hasLength(token)) { + throw new IllegalArgumentException( + "A Vault token must be supplied by a token provider"); + } + return VaultToken.of(token); + } + + } + +} diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java index 4d49b997..8aed15a6 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactory.java @@ -16,27 +16,15 @@ package org.springframework.cloud.config.server.environment.vault; -import java.net.URI; - import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.cloud.config.server.environment.ConfigTokenProvider; import org.springframework.cloud.config.server.environment.EnvironmentRepositoryFactory; import org.springframework.cloud.config.server.environment.EnvironmentWatch; import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties; -import org.springframework.util.StringUtils; -import org.springframework.vault.VaultException; -import org.springframework.vault.authentication.ClientAuthentication; -import org.springframework.vault.authentication.SimpleSessionManager; -import org.springframework.vault.client.RestTemplateBuilder; -import org.springframework.vault.client.VaultClients; -import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.core.VaultKeyValueOperations; import org.springframework.vault.core.VaultKeyValueOperationsSupport; import org.springframework.vault.core.VaultTemplate; -import org.springframework.vault.support.VaultToken; -import org.springframework.web.util.UriComponentsBuilder; /** * @author Dylan Roberts @@ -49,23 +37,20 @@ public class SpringVaultEnvironmentRepositoryFactory implements private final EnvironmentWatch watch; - private final ConfigTokenProvider tokenProvider; + private final SpringVaultClientConfiguration clientConfiguration; public SpringVaultEnvironmentRepositoryFactory( ObjectProvider request, EnvironmentWatch watch, - ConfigTokenProvider tokenProvider) { + SpringVaultClientConfiguration clientConfiguration) { this.request = request; this.watch = watch; - this.tokenProvider = tokenProvider; + this.clientConfiguration = clientConfiguration; } @Override public SpringVaultEnvironmentRepository build( VaultEnvironmentProperties vaultProperties) { - RestTemplateBuilder restTemplateBuilder = buildRestTemplateBuilder( - vaultProperties); - - VaultTemplate vaultTemplate = buildVaultTemplate(restTemplateBuilder); + VaultTemplate vaultTemplate = clientConfiguration.vaultTemplate(); VaultKeyValueOperations accessStrategy = buildVaultAccessStrategy(vaultProperties, vaultTemplate); @@ -74,29 +59,6 @@ public class SpringVaultEnvironmentRepositoryFactory implements vaultProperties, accessStrategy); } - private RestTemplateBuilder buildRestTemplateBuilder( - VaultEnvironmentProperties vaultProperties) { - URI baseUrl = UriComponentsBuilder.newInstance() - .scheme(vaultProperties.getScheme()).host(vaultProperties.getHost()) - .port(vaultProperties.getPort()).build().toUri(); - - RestTemplateBuilder restTemplateBuilder = RestTemplateBuilder.builder() - .endpoint(VaultEndpoint.from(baseUrl)); - - if (vaultProperties.getNamespace() != null) { - restTemplateBuilder.customizers( - restTemplate -> restTemplate.getInterceptors().add(VaultClients - .createNamespaceInterceptor(vaultProperties.getNamespace()))); - } - - return restTemplateBuilder; - } - - private VaultTemplate buildVaultTemplate(RestTemplateBuilder restTemplateBuilder) { - return new VaultTemplate(restTemplateBuilder, new SimpleSessionManager( - new ConfigTokenProviderAuthentication(tokenProvider))); - } - private VaultKeyValueOperations buildVaultAccessStrategy( VaultEnvironmentProperties vaultProperties, VaultTemplate vaultTemplate) { String backend = vaultProperties.getBackend(); @@ -115,25 +77,4 @@ public class SpringVaultEnvironmentRepositoryFactory implements } } - public static class ConfigTokenProviderAuthentication - implements ClientAuthentication { - - private final ConfigTokenProvider tokenProvider; - - public ConfigTokenProviderAuthentication(ConfigTokenProvider tokenProvider) { - this.tokenProvider = tokenProvider; - } - - @Override - public VaultToken login() throws VaultException { - String token = tokenProvider.getToken(); - if (!StringUtils.hasLength(token)) { - throw new IllegalArgumentException( - "A Vault token must be supplied by a token provider"); - } - return VaultToken.of(token); - } - - } - } diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfigurationTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfigurationTests.java new file mode 100644 index 00000000..5a1925da --- /dev/null +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultClientConfigurationTests.java @@ -0,0 +1,395 @@ +/* + * Copyright 2018-2019 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.config.server.environment.vault; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Base64; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties; +import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod; +import org.springframework.cloud.config.server.environment.vault.SpringVaultClientConfiguration.ConfigTokenProviderAuthentication; +import org.springframework.core.io.ClassPathResource; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; +import org.springframework.vault.authentication.AwsEc2Authentication; +import org.springframework.vault.authentication.AwsIamAuthentication; +import org.springframework.vault.authentication.AzureMsiAuthentication; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.ClientCertificateAuthentication; +import org.springframework.vault.authentication.CubbyholeAuthentication; +import org.springframework.vault.authentication.GcpComputeAuthentication; +import org.springframework.vault.authentication.GcpIamAuthentication; +import org.springframework.vault.authentication.KubernetesAuthentication; +import org.springframework.vault.authentication.PcfAuthentication; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import org.springframework.vault.support.VaultToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.AWS_EC2; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.AWS_IAM; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.AZURE_MSI; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.CERT; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.CUBBYHOLE; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.GCP_GCE; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.GCP_IAM; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.KUBERNETES; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.PCF; +import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.TOKEN; + +class SpringVaultClientConfigurationTests { + + @Test + public void appRoleRoleIdProvidedSecretIdProvided() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.getAppRole().setRoleId("foo"); + properties.getAppRole().setSecretId("bar"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getRoleId()).isInstanceOf(RoleId.provided("foo").getClass()); + assertThat(options.getSecretId()) + .isInstanceOf(SecretId.provided("bar").getClass()); + } + + @Test + public void appRoleRoleIdProvidedSecretIdAbsent() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.getAppRole().setRoleId("foo"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getRoleId()).isInstanceOf(RoleId.provided("foo").getClass()); + assertThat(options.getSecretId()).isInstanceOf(SecretId.absent().getClass()); + } + + @Test + public void appRoleRoleIdProvidedSecretIdPull() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setToken("token"); + properties.getAppRole().setRoleId("foo"); + properties.getAppRole().setRole("my-role"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getAppRole()).isEqualTo("my-role"); + assertThat(options.getRoleId()).isInstanceOf(RoleId.provided("foo").getClass()); + assertThat(options.getSecretId()) + .isInstanceOf(SecretId.pull(VaultToken.of("token")).getClass()); + } + + @Test + public void appRoleWithFullPull() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setToken("token"); + properties.getAppRole().setRole("my-role"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getAppRole()).isEqualTo("my-role"); + assertThat(options.getRoleId()) + .isInstanceOf(RoleId.pull(VaultToken.of("token")).getClass()); + assertThat(options.getSecretId()) + .isInstanceOf(SecretId.pull(VaultToken.of("token")).getClass()); + } + + @Test + public void appRoleFullWrapped() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setToken("token"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getRoleId()) + .isInstanceOf(RoleId.wrapped(VaultToken.of("token")).getClass()); + assertThat(options.getSecretId()) + .isInstanceOf(SecretId.wrapped(VaultToken.of("token")).getClass()); + } + + @Test + public void appRoleRoleIdWrappedSecretIdProvided() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setToken("token"); + properties.getAppRole().setSecretId("bar"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getRoleId()) + .isInstanceOf(RoleId.wrapped(VaultToken.of("token")).getClass()); + assertThat(options.getSecretId()) + .isInstanceOf(SecretId.provided("bar").getClass()); + } + + @Test + public void appRoleRoleIdProvidedSecretIdWrapped() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setToken("token"); + properties.getAppRole().setRoleId("foo"); + + AppRoleAuthenticationOptions options = SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties); + + assertThat(options.getRoleId()).isInstanceOf(RoleId.provided("foo").getClass()); + assertThat(options.getSecretId()) + .isInstanceOf(SecretId.wrapped(VaultToken.of("token")).getClass()); + } + + @Test + public void appRoleWithUnconfiguredRoleId() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + + assertThatThrownBy(() -> SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void appRoleWithUnconfiguredRoleIdIfRoleNameSet() { + + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.getAppRole().setRole("my-role"); + + assertThatThrownBy(() -> SpringVaultClientConfiguration + .getAppRoleAuthenticationOptions(properties)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void defaultAuthentication() { + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + + assertClientAuthenticationOfType(properties, + ConfigTokenProviderAuthentication.class); + } + + @Test + public void awsEc2Authentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(AWS_EC2); + properties.getAwsEc2().setRole("server"); + properties.getAwsEc2().setAwsEc2Path("aws-ec2"); + + assertClientAuthenticationOfType(properties, AwsEc2Authentication.class); + } + + @Test + public void awsIamAuthentication() { + System.setProperty("aws.accessKeyId", "access-key-id"); + System.setProperty("aws.secretKey", "secret-key"); + + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(AWS_IAM); + properties.getAwsIam().setRole("server"); + properties.getAwsIam().setAwsPath("aws-iam"); + + assertClientAuthenticationOfType(properties, AwsIamAuthentication.class); + } + + @Test + public void azureMsiAuthentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(AZURE_MSI); + properties.getAzureMsi().setRole("server"); + properties.getAzureMsi().setAzurePath("azure-msi"); + + assertClientAuthenticationOfType(properties, AzureMsiAuthentication.class); + } + + @Test + public void clientCertificateAuthentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(CERT); + + assertClientAuthenticationOfType(properties, + ClientCertificateAuthentication.class); + } + + @Test + public void cubbyholeAuthentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(CUBBYHOLE); + properties.setToken("token"); + + assertClientAuthenticationOfType(properties, CubbyholeAuthentication.class); + } + + @Test + public void gcpComputeAuthentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(GCP_GCE); + properties.getGcpGce().setRole("server"); + properties.getGcpGce().setServiceAccount("service-account"); + + assertClientAuthenticationOfType(properties, GcpComputeAuthentication.class); + } + + @Test + public void gcpIamAuthentication() { + final String GCE_JSON = "{" + " \"type\": \"service_account\"," + + " \"project_id\": \"project\"," + + " \"private_key_id\": \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"," + + " \"private_key\": \"" + + "-----BEGIN PRIVATE KEY-----\\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5qHafKgP/FAKE\\n" + + "xfRl0i47zXKbGQJvGAGpcmiXRgeWkZp+kwNwBguOYNwO1qDcmewKvMPazj7EL0hV\\n" + + "XMkPxgshZ9ZSxPwg7/XHHcyGCYBJhDc2hyunQvc2WGUOlQKg/nOlq3Dg8d9c/0yF\\n" + + "mFOh2K+IrbV6Vqs3nXsupV1q2FbUCVg6NGB0HCdTBZO4e36tmcaWgC1cKTv/Nh+j\\n" + + "f2Bf7qBTk0GOL9AjKoa/HP24Yto5zFoOFLU+2ZkVbb8hhO8OMUKW8dLIynqqRqwv\\n" + + "oI8e4oiHX3dBvwcS0zZkEUtQiDI80OCbU7ZhPgn5xQpndanD9dZ4TYSgKuXRVTzr\\n" + + "1cyoyP7HAgMBAAECggEAV2fOYOSg+V60WvYhN4aaKaFxoT9G/BJrReENCJr5m5N1\\n" + + "Dr4b0jOmYSOMtpepJ/J3RB7Wfj63Ihm4jieeqQRt3Q5Lwq/mm4MdTN7kmP4EHZhX\\n" + + "fh5pGNfYFwfKm/DfSfhBbe+mtuBobhnrZsHuLbYb/db6J1yCQy6q/azwrAqp5iyq\\n" + + "GjNN+WiDIcrydPKKiaszMnb9mNH+Y6Ianx1mvSLT35nBEF6Z4rJVERl26diOoo3I\\n" + + "F6WadIwTqcoLo5duUO3SaHKKLcoSEEaGkutuTCHcFOzhvZrXbuIyD567Vp5oVFe9\\n" + + "SHN10vceQQdWPh2UsKrVQfIdc70+9tlslka5X6BbIQKBgQDss/APs65NNev2lPdb\\n" + + "Jzdd+0YwKEQXeENWkU1xJJkNH/wF0ZGuYxoKafZR0efs1LnrbaPfHveFCnUcDuXU\\n" + + "yDnzG2zMw1Q72F8eGHpLItPSh0ZkfSlN58uM1oYMdTFUE6ezlOYEnKIYdhjmQWiE\\n" + + "uEa1G4ZW0aX0NLICet597GnLKQKBgQDIyzRVbOOzOxGUgrWT0RPT12VVNCn7edn1\\n" + + "UWLKDl4L2uF8vE4g8WW7gwNkVbuO3VPKqdGuCBDfVyyysOOOCDN0IxSxDk3458VY\\n" + + "4I3jAuBcgDsixwC28l0QtFnz2yRuD2fsBhLnoSfsM/T2hNbf7atDtMQhbbgU37me\\n" + + "X+Ewtr+obwKBgCdbb/IcbUH3UknI0Sw95A3jZvNA7rl8TK4LMPY8IJq3E7+f7foy\\n" + + "DjVnEwbdwRN294b2zwWdb4iWiYxlyb9Mn54VlEyjudDNlFs7tLHjk5bw2TqCOSjz\\n" + + "/rtnPBi8L7yMHBlXC7v+k1E/6bn3bDqNLWyVrAuphk+Jp4OUDIShl6GpAoGAFIAC\\n" + + "mNIkMTFPqyzpIu1Oq+sq0lcgDiezpAMqJdzvpyAys0x6YYyjyVAn8X97Rau9GUzb\\n" + + "NnxmVJcO3jOHGAIoVqwaObVvKoFnOZq7gbjSdT82Smes4ADAlasEIAx4nK//+S3p\\n" + + "kjJ24/ut/9kyIuyd9qym9Y7BI4hv6AZ79EBEMwsCgYEAgXzq5+NCfJIi6Zduugym\\n" + + "iUU3y/3CWc/pLhnw3XZ5r3M5fLXokLhLU6FsNflTpdcf2QoNL58mE0tanPqg09Xh\\n" + + "7fHWR/8rISt2TsMlqFjc5rQxWg8yRpdd5Ti/Ln8v7EV3RGbhFlOqlC9hiyqfyd7V\\n" + + "qZjZg4zUxPO1I8ae8hbGMWs=\\n" + "-----END PRIVATE KEY-----\\n\"," + + " \"client_email\": \"test@example.com\"," + + " \"client_id\": \"111111111111111111111\"," + + " \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\"," + + " \"token_uri\": \"https://accounts.google.com/o/oauth2/token\"," + + " \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\"," + + " \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/toolsmiths-pcf-sa%40cf-spinnaker.iam.gserviceaccount.com\"" + + "}"; + + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(GCP_IAM); + properties.getGcpIam().setRole("server"); + properties.getGcpIam().setProjectId("project"); + properties.getGcpIam().setServiceAccountId("service-account"); + properties.getGcpIam().getCredentials().setEncodedKey(base64(GCE_JSON)); + + assertClientAuthenticationOfType(properties, GcpIamAuthentication.class); + } + + @Test + public void kuberneteAuthentication() throws IOException { + Files.write(Paths.get("target", "token"), "token".getBytes()); + + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(KUBERNETES); + properties.getKubernetes().setRole("server"); + properties.getKubernetes().setServiceAccountTokenFile("target/token"); + + assertClientAuthenticationOfType(properties, KubernetesAuthentication.class); + } + + @Test + public void pcfAuthentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(PCF); + properties.getPcf().setRole("my-role"); + properties.getPcf() + .setInstanceKey(new ClassPathResource("configserver-test.yml")); + properties.getPcf() + .setInstanceCertificate(new ClassPathResource("configserver-test.yml")); + + assertClientAuthenticationOfType(properties, PcfAuthentication.class); + } + + @Test + public void tokenAuthentication() { + VaultEnvironmentProperties properties = getPropertiesForAuthMethod(TOKEN); + properties.setToken("token"); + + assertClientAuthenticationOfType(properties, TokenAuthentication.class); + } + + @Test + public void defaultSslConfiguration() { + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + + SpringVaultClientConfiguration configuration = getConfiguration(properties); + SslConfiguration sslConfiguration = configuration.sslConfiguration(); + + assertThat(sslConfiguration.getKeyStoreConfiguration()) + .isEqualTo(KeyStoreConfiguration.unconfigured()); + assertThat(sslConfiguration.getTrustStoreConfiguration()) + .isEqualTo(KeyStoreConfiguration.unconfigured()); + } + + @Test + public void customSslConfiguration() { + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.getSsl().setKeyStore(new ClassPathResource("ssl-test.jks")); + properties.getSsl().setKeyStorePassword("password"); + properties.getSsl().setTrustStore(new ClassPathResource("ssl-test.jks")); + properties.getSsl().setTrustStorePassword("password"); + + SpringVaultClientConfiguration configuration = getConfiguration(properties); + SslConfiguration sslConfiguration = configuration.sslConfiguration(); + + KeyStoreConfiguration keyStoreConfiguration = sslConfiguration + .getKeyStoreConfiguration(); + KeyStoreConfiguration trustStoreConfiguration = sslConfiguration + .getTrustStoreConfiguration(); + assertThat(keyStoreConfiguration.isPresent()).isTrue(); + assertThat(new String(keyStoreConfiguration.getStorePassword())) + .isEqualTo("password"); + assertThat(trustStoreConfiguration.isPresent()).isTrue(); + assertThat(new String(trustStoreConfiguration.getStorePassword())) + .isEqualTo("password"); + } + + private VaultEnvironmentProperties getPropertiesForAuthMethod( + AuthenticationMethod authMethod) { + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setAuthentication(authMethod); + return properties; + } + + private void assertClientAuthenticationOfType(VaultEnvironmentProperties properties, + Class type) { + ClientAuthentication clientAuthentication = getConfiguration(properties) + .clientAuthentication(); + + assertThat(clientAuthentication).isInstanceOf(type); + } + + private SpringVaultClientConfiguration getConfiguration( + VaultEnvironmentProperties properties) { + return new SpringVaultClientConfiguration(properties, () -> null); + } + + private String base64(String value) { + return new String(Base64.getEncoder().encode(value.getBytes())); + } + +} diff --git a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java index b82eadcf..be1fb0ad 100644 --- a/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java +++ b/spring-cloud-config-server/src/test/java/org/springframework/cloud/config/server/environment/vault/SpringVaultEnvironmentRepositoryFactoryTests.java @@ -21,11 +21,13 @@ import javax.servlet.http.HttpServletRequest; import org.junit.Test; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.cloud.config.server.environment.ConfigTokenProvider; import org.springframework.cloud.config.server.environment.EnvironmentWatch; import org.springframework.cloud.config.server.environment.VaultEnvironmentProperties; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.core.VaultKeyValueOperations; import org.springframework.vault.core.VaultKeyValueOperationsSupport; +import org.springframework.vault.core.VaultTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -41,8 +43,8 @@ public class SpringVaultEnvironmentRepositoryFactoryTests { VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); SpringVaultEnvironmentRepository environmentRepository = new SpringVaultEnvironmentRepositoryFactory( - mockHttpRequest(), new EnvironmentWatch.Default(), mockTokenProvider()) - .build(properties); + mockHttpRequest(), new EnvironmentWatch.Default(), + mockClientConfiguration()).build(properties); VaultKeyValueOperations keyValueTemplate = environmentRepository .getKeyValueTemplate(); @@ -56,8 +58,8 @@ public class SpringVaultEnvironmentRepositoryFactoryTests { properties.setKvVersion(2); SpringVaultEnvironmentRepository environmentRepository = new SpringVaultEnvironmentRepositoryFactory( - mockHttpRequest(), new EnvironmentWatch.Default(), mockTokenProvider()) - .build(properties); + mockHttpRequest(), new EnvironmentWatch.Default(), + mockClientConfiguration()).build(properties); VaultKeyValueOperations keyValueTemplate = environmentRepository .getKeyValueTemplate(); @@ -65,10 +67,16 @@ public class SpringVaultEnvironmentRepositoryFactoryTests { .isEqualTo(VaultKeyValueOperationsSupport.KeyValueBackend.KV_2); } - private ConfigTokenProvider mockTokenProvider() { - ConfigTokenProvider tokenProvider = mock(ConfigTokenProvider.class); - when(tokenProvider.getToken()).thenReturn("token"); - return tokenProvider; + private SpringVaultClientConfiguration mockClientConfiguration() { + VaultTemplate vaultTemplate = new VaultTemplate( + VaultEndpoint.create("localhost", 8200), + new TokenAuthentication("token")); + + SpringVaultClientConfiguration clientConfiguration = mock( + SpringVaultClientConfiguration.class); + when(clientConfiguration.vaultTemplate()).thenReturn(vaultTemplate); + + return clientConfiguration; } @SuppressWarnings("unchecked")