spring-projects / spring-vault

Provides familiar Spring abstractions for HashiCorp Vault
https://spring.io/projects/spring-vault
Apache License 2.0
283 stars 186 forks source link

vault secret repository SpEL values #598

Closed iamlothian closed 3 years ago

iamlothian commented 3 years ago

versions: org.springframework.boot:spring-boot-starter-web:2.3.0.RELEASE org.springframework.boot:spring-boot-starter-actuator:2.3.0.RELEASE org.springframework.boot:spring-boot-starter-security:2.3.0.RELEASE org.springframework.boot:spring-boot-starter-test:2.3.0.RELEASE org.springframework.cloud:spring-cloud-starter-vault-config:2.2.2.RELEASE org.springframework.cloud:spring-cloud-vault-config-databases:2.2.2.RELEASE org.springframework.data:spring-data-keyvalue:2.2.3.RELEASE

The @Secret annotation's documentation states:

The prefix to distinguish between domain types. The attribute supports SpEL expressions to dynamically calculate the keyspace based on a per-operation basis.

@Data
@Secret(value = "${spring.application.name}", backend = "secret")
public class Secret {
    @Id
    private String id;
    private String secretId;
}

But when I try to use property templates I get an error from the rest template used to build the request to vault.

Caused by: java.lang.IllegalArgumentException: Not enough variable values available to expand 'spring.application.name'
    at org.springframework.web.util.UriComponents$VarArgsTemplateVariables.getValue(UriComponents.java:367)
    at org.springframework.web.util.UriComponents.expandUriComponent(UriComponents.java:262)
    at org.springframework.web.util.HierarchicalUriComponents$FullPathComponent.expand(HierarchicalUriComponents.java:883)
    at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:429)
    at org.springframework.web.util.HierarchicalUriComponents.expandInternal(HierarchicalUriComponents.java:51)
    at org.springframework.web.util.UriComponents.expand(UriComponents.java:172)
    at org.springframework.web.util.UriComponentsBuilder.build(UriComponentsBuilder.java:438)
    at org.springframework.web.util.DefaultUriBuilderFactory.expand(DefaultUriBuilderFactory.java:205)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:676)
    at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:421)
    at org.springframework.vault.core.VaultTemplate.lambda$write$3(VaultTemplate.java:342)
    at org.springframework.vault.core.VaultTemplate.doWithSession(VaultTemplate.java:388)
    at org.springframework.vault.core.VaultTemplate.write(VaultTemplate.java:342)
    at org.springframework.vault.repository.core.VaultKeyValueAdapter.put(VaultKeyValueAdapter.java:84)
    at org.springframework.data.keyvalue.core.KeyValueTemplate.lambda$update$1(KeyValueTemplate.java:204)
    at org.springframework.data.keyvalue.core.KeyValueTemplate.execute(KeyValueTemplate.java:345)
    ... 114 more

It seems that the value SpEL template is not being interpolated before the value is provided to the restTemplate which is assuming the {..} values are uri template values.

Have I misunderstood this feature?

My alternative is writing my own repository using the vaultTemplate.

mp911de commented 3 years ago

SpEL support follows Spring conventions for SpEL evaluation by using #{…}. The Spring Data SpEL context has no access to Spring's Environment for property expansion (${…}) but rather makes use of the target object and contextual extensions. You can find an example at BasicVaultPersistentEntityUnitTests.

iamlothian commented 3 years ago

You're totally right! Let me see if I can rework my solution to make a spring property exposed via contextual expression.

For clarity, the secret path I want to use can be constructed from "/secret/${spring.application.name}/${env}/".

mp911de commented 3 years ago

Context extensions are regular beans. You could build an extension that holds Environment and exposes both properties similar to the sample.

iamlothian commented 3 years ago

Thanks, @mp911de that's how I did it.

For those who find this and want more examples than the test:

application.yaml

credentials:
  backend: "secret"
  path: "/dev/credentials/"

CredentialProperties.java

@AllArgsConstructor
public class CredentialProperties implements EvaluationContextExtension {

    private final String backend;
    private final String path;

    @Override
    public String getExtensionId() {
        return "credentialProperties";
    }

    @Override
    public Map<String, Object> getProperties() {
        Map<String, Object> properties = new LinkedHashMap<>();
        properties.put("credentialBackend", backend);
        properties.put("credentialPath", path);
        return properties;
    }

}

ApplicationConfig.java

@Configuration
public class ApplicationConfig {

   @Bean
    CredentialProperties credentialProperties(
            @Value("${spring.application.name}") String context,
            @Value("${credentials.backend}") String backend,
            @Value("${credentials.path}") String path
    ) {
        return new VaultEnvironmentCredentialProperties(backend, context + path);
    }
}

CredentialSecret.java

@Data
@Secret(value = "#{credentialPath}", backend = "#{credentialBackend}")
public class CredentialSecret {
    @Id
    private String id;
    private String secretId;
}
mgalindor commented 2 years ago

As an alternative you can create a ConfigurationProperties and call the fields in the @Secret

@Component("fooProp")
@ConfigurationProperties("foo")
public class FooProperties{
    Stirng name;
    String backend; 
} 

@Secret( value="#{@fooProp.name}", backend="#{@fooProp.backend}" )
public class VaultTest{..... }