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 f561beb2..f82b58df 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 @@ -525,10 +525,28 @@ public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProp */ private String role = ""; + /** + * URI to the Azure MSI Identity Service. + */ + private String identityTokenService = ""; + + /** + * URI to the Azure MSI Metadata Service. + */ + private String metadataService = ""; + public String getAzurePath() { return this.azurePath; } + public String getIdentityTokenService() { + return identityTokenService; + } + + public String getMetadataService() { + return metadataService; + } + public String getRole() { return this.role; } @@ -537,6 +555,14 @@ public class VaultEnvironmentProperties implements HttpEnvironmentRepositoryProp this.azurePath = azurePath; } + public void setIdentityTokenService(String identityTokenService) { + this.identityTokenService = identityTokenService; + } + + public void setMetadataService(String metadataService) { + this.metadataService = metadataService; + } + public void setRole(String role) { this.role = role; } 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 index a08e8118..5009583d 100644 --- 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 @@ -103,9 +103,7 @@ public class SpringVaultClientConfiguration extends AbstractVaultConfiguration endpointProvider, requestFactory); if (vaultProperties.getNamespace() != null) { - restTemplateBuilder.customizers( - restTemplate -> restTemplate.getInterceptors().add(VaultClients - .createNamespaceInterceptor(vaultProperties.getNamespace()))); + restTemplateBuilder.customizers(this::applyNamespaceInterceptor); } return restTemplateBuilder; @@ -132,6 +130,19 @@ public class SpringVaultClientConfiguration extends AbstractVaultConfiguration return new SslConfiguration(keyStoreConfiguration, trustStoreConfiguration); } + /** + * This method is a work-around for the Spring Vault issue documented in + * https://github.com/spring-projects/spring-vault/issues/546. The method should be + * removed when Spring Cloud Config is upgraded to the version of Spring Vault that + * includes the fix for the issue. + * @return the {@link RestOperations} to be used for Vault access + */ + @Override + public RestOperations restOperations() { + return restTemplateBuilder(vaultEndpointProvider(), + clientHttpRequestFactoryWrapper().getClientHttpRequestFactory()).build(); + } + private SslConfiguration.KeyStoreConfiguration getKeyStoreConfiguration( Resource resourceProperty, String passwordProperty) { @@ -147,6 +158,15 @@ public class SpringVaultClientConfiguration extends AbstractVaultConfiguration return SslConfiguration.KeyStoreConfiguration.of(resourceProperty); } + private RestOperations applyNamespaceInterceptor(RestTemplate restTemplate) { + if (vaultProperties.getNamespace() != null) { + restTemplate.getInterceptors().add(VaultClients + .createNamespaceInterceptor(vaultProperties.getNamespace())); + } + + return restTemplate; + } + /** * @return a new {@link ClientAuthentication}. */ diff --git a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/authentication/AzureMsiClientAuthenticationProvider.java b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/authentication/AzureMsiClientAuthenticationProvider.java index 9da54df4..901aaa1f 100644 --- a/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/authentication/AzureMsiClientAuthenticationProvider.java +++ b/spring-cloud-config-server/src/main/java/org/springframework/cloud/config/server/environment/vault/authentication/AzureMsiClientAuthenticationProvider.java @@ -16,6 +16,8 @@ package org.springframework.cloud.config.server.environment.vault.authentication; +import java.net.URI; + 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.SpringVaultClientAuthenticationProvider; @@ -44,10 +46,22 @@ public class AzureMsiClientAuthenticationProvider AuthenticationMethod.AZURE_MSI)); AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder() - .role(azureMsi.getRole()).build(); + .role(azureMsi.getRole()).path(azureMsi.getAzurePath()) + .instanceMetadataUri(getUri(azureMsi.getMetadataService(), + AzureMsiAuthenticationOptions.DEFAULT_INSTANCE_METADATA_SERVICE_URI)) + .identityTokenServiceUri(getUri(azureMsi.getIdentityTokenService(), + AzureMsiAuthenticationOptions.DEFAULT_IDENTITY_TOKEN_SERVICE_URI)) + .build(); return new AzureMsiAuthentication(options, vaultRestOperations, externalRestOperations); } + private URI getUri(String uriString, URI defaultUri) { + if (uriString == null || uriString.isEmpty()) { + return defaultUri; + } + return URI.create(uriString); + } + } 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 index 2641a22c..7d3f6b61 100644 --- 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 @@ -25,6 +25,7 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.context.annotation.UserConfigurations; @@ -45,11 +46,17 @@ import org.springframework.cloud.config.server.environment.vault.authentication. import org.springframework.cloud.config.server.environment.vault.authentication.PcfClientAuthenticationProvider; import org.springframework.cloud.config.server.environment.vault.authentication.TokenClientAuthenticationProvider; import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; import org.springframework.vault.authentication.AppRoleAuthentication; import org.springframework.vault.authentication.AwsEc2Authentication; import org.springframework.vault.authentication.AwsIamAuthentication; 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; @@ -58,8 +65,11 @@ 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.client.VaultHttpHeaders; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.cloud.config.server.environment.VaultEnvironmentProperties.AuthenticationMethod.APPROLE; @@ -73,6 +83,8 @@ import static org.springframework.cloud.config.server.environment.VaultEnvironme 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; +import static org.springframework.vault.authentication.AzureMsiAuthenticationOptions.DEFAULT_IDENTITY_TOKEN_SERVICE_URI; +import static org.springframework.vault.authentication.AzureMsiAuthenticationOptions.DEFAULT_INSTANCE_METADATA_SERVICE_URI; class SpringVaultClientConfigurationTests { @@ -137,6 +149,16 @@ class SpringVaultClientConfigurationTests { properties.getAzureMsi().setAzurePath("azure-msi"); assertClientAuthenticationOfType(properties, AzureMsiAuthentication.class); + + AzureMsiAuthentication clientAuthentication = (AzureMsiAuthentication) getConfiguration( + properties).clientAuthentication(); + AzureMsiAuthenticationOptions options = (AzureMsiAuthenticationOptions) ReflectionTestUtils + .getField(clientAuthentication, "options"); + + assertThat(options.getIdentityTokenServiceUri()) + .isEqualTo(DEFAULT_IDENTITY_TOKEN_SERVICE_URI); + assertThat(options.getInstanceMetadataServiceUri()) + .isEqualTo(DEFAULT_INSTANCE_METADATA_SERVICE_URI); } @Test @@ -280,6 +302,42 @@ class SpringVaultClientConfigurationTests { .isEqualTo("password"); } + @Test + public void namespaceHeaderNotAddedWhenNamespaceNotConfigured() throws IOException { + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + + SpringVaultClientConfiguration configuration = getConfiguration(properties); + HttpRequest request = invokeInterceptors(configuration.restOperations()); + assertThat(request.getHeaders().getFirst(VaultHttpHeaders.VAULT_NAMESPACE)) + .isNull(); + } + + @Test + public void namespaceInterceptorAddedWhenNamespaceConfigured() throws IOException { + VaultEnvironmentProperties properties = new VaultEnvironmentProperties(); + properties.setNamespace("test-namespace"); + + SpringVaultClientConfiguration configuration = getConfiguration(properties); + HttpRequest request = invokeInterceptors(configuration.restOperations()); + assertThat(request.getHeaders().getFirst(VaultHttpHeaders.VAULT_NAMESPACE)) + .isEqualTo("test-namespace"); + } + + private HttpRequest invokeInterceptors(RestOperations restOperations) + throws IOException { + assertThat(restOperations).isInstanceOf(RestTemplate.class); + RestTemplate restTemplate = (RestTemplate) restOperations; + + MockClientHttpRequest request = new MockClientHttpRequest(); + ClientHttpRequestExecution execution = Mockito + .mock(ClientHttpRequestExecution.class); + byte[] body = new byte[] {}; + for (ClientHttpRequestInterceptor interceptor : restTemplate.getInterceptors()) { + interceptor.intercept(request, body, execution); + } + return request; + } + private void assertClientAuthenticationOfType(VaultEnvironmentProperties properties, Class type) { ClientAuthentication clientAuthentication = getConfiguration(properties)