From b916d4c6e7cf5e2f73f1fb48410f5e6753e90d7a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Aug 2017 15:21:20 +0200 Subject: [PATCH] Vault repository support. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support Spring Data Repositories via Spring Data's KeyValue module. Domain objects can be mapped to JSON using a custom converter and created, update, deleted and queried using the repository abstraction. Vault repositories support query derivation limited to predicates on the Id property with paging and sorting. @Configuration @EnableVaultRepositories public class ApplicationConfig { @Bean public VaultTemplate vaultTemplate() { return new VaultTemplate(…); } } @Test public void loadAndSave() { Credentials heisenberg = new Credentials(); heisenberg.setId("heisenberg"); heisenberg.setPassword("327215"); vaultRepository.save(heisenberg); Iterable all = vaultRepository.findAll(); // } interface CredentialsRepository extends PagingAndSortingRepository { } @Data public class Credentials { @Id String id; String password; } GET https://localhost:8200/v1/secret/credentials/heisenberg HTTP/1.1 200 OK Content-Type: application/json { // … "renewable": false, "lease_duration": …, "data": { "_class": "com.example.Credentials", "password": "327215" }, // … } See gh-128. --- pom.xml | 71 +- spring-vault-core/pom.xml | 18 +- .../authentication/AwsEc2Authentication.java | 2 +- .../CachingVaultTokenSupplier.java | 2 +- .../vault/support/SslConfiguration.java | 2 +- spring-vault-repository/pom.xml | 136 ++++ .../EnableVaultRepositories.java | 136 ++++ .../VaultRepositoriesRegistrar.java | 41 + ...VaultRepositoryConfigurationExtension.java | 217 +++++ .../convert/AbstractVaultConverter.java | 84 ++ .../convert/DefaultVaultTypeMapper.java | 138 ++++ .../convert/MappingVaultConverter.java | 757 ++++++++++++++++++ .../repository/convert/SecretDocument.java | 137 ++++ .../convert/SecretDocumentAccessor.java | 262 ++++++ .../repository/convert/VaultConverter.java | 30 + .../convert/VaultCustomConversions.java | 97 +++ .../repository/convert/VaultTypeMapper.java | 37 + .../repository/convert/package-info.java | 8 + .../core/MappingVaultEntityInformation.java | 49 ++ .../core/VaultEntityInformation.java | 28 + .../repository/core/VaultKeyValueAdapter.java | 238 ++++++ .../core/VaultKeyValueTemplate.java | 56 ++ .../repository/core/VaultQueryEngine.java | 200 +++++ .../mapping/BasicVaultPersistentEntity.java | 71 ++ .../vault/repository/mapping/Secret.java | 57 ++ .../mapping/VaultMappingContext.java | 77 ++ .../mapping/VaultPersistentEntity.java | 34 + .../mapping/VaultPersistentProperty.java | 60 ++ .../repository/mapping/VaultSimpleTypes.java | 48 ++ .../vault/repository/query/VaultQuery.java | 175 ++++ .../repository/query/VaultQueryCreator.java | 259 ++++++ .../support/VaultRepositoryFactory.java | 66 ++ .../support/VaultRepositoryFactoryBean.java | 57 ++ .../VaultIntegrationTestConfiguration.java | 48 ++ .../VaultRepositoryIntegrationTests.java | 161 ++++ .../DefaultVaultTypeMapperUnitTests.java | 221 +++++ .../MappingVaultConverterUnitTests.java | 380 +++++++++ .../BasicVaultPersistentEntityUnitTests.java | 60 ++ .../mapping/VaultMappingContextUnitTests.java | 66 ++ .../query/VaultQueryCreatorUnitTests.java | 236 ++++++ src/main/asciidoc/new-features.adoc | 1 + .../reference/vault-repositories.adoc | 276 +++++++ src/main/asciidoc/reference/vault.adoc | 2 + 43 files changed, 5064 insertions(+), 37 deletions(-) create mode 100644 spring-vault-repository/pom.xml create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/EnableVaultRepositories.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoriesRegistrar.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoryConfigurationExtension.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/AbstractVaultConverter.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapper.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/MappingVaultConverter.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultTypeMapper.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/package-info.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/core/MappingVaultEntityInformation.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultEntityInformation.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultQueryEngine.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntity.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/Secret.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultMappingContext.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentEntity.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentProperty.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultSimpleTypes.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQuery.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQueryCreator.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java create mode 100644 spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactoryBean.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultIntegrationTestConfiguration.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultRepositoryIntegrationTests.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapperUnitTests.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/MappingVaultConverterUnitTests.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntityUnitTests.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/VaultMappingContextUnitTests.java create mode 100644 spring-vault-repository/src/test/java/org/springframework/vault/repository/query/VaultQueryCreatorUnitTests.java create mode 100644 src/main/asciidoc/reference/vault-repositories.adoc diff --git a/pom.xml b/pom.xml index 19c02877..2a9c6c09 100644 --- a/pom.xml +++ b/pom.xml @@ -15,17 +15,20 @@ spring-vault-dependencies spring-vault-core + spring-vault-repository spring-vault-distribution UTF-8 5.0.0.RC3 + Kay-RC2 Bismuth-M3 1.8 multi spring-vault ${basedir} + ${project.build.directory}/shared-resources 2016 @@ -116,6 +119,15 @@ import + + + org.springframework.data + spring-data-releasetrain + ${spring-data-releasetrain.version} + pom + import + + @@ -231,6 +243,12 @@ 3.5.1 + + org.apache.maven.plugins + maven-jar-plugin + 3.0.2 + + org.apache.maven.plugins maven-surefire-plugin @@ -247,6 +265,26 @@ org.apache.maven.plugins maven-javadoc-plugin 2.10.4 + + true +
${project.name}
+ ${java.version} + true + ${shared.resources}/javadoc + ${shared.resources}/javadoc/overview.html + + ${shared.resources}/javadoc/spring-javadoc.css + + + true + -Xdoclint:none + + https://docs.spring.io/spring-data/commons/docs/current/api + https://docs.spring.io/spring-data/keyvalue/docs/current/api + http://docs.spring.io/spring/docs/current/javadoc-api + http://docs.oracle.com/javase/8/docs/api + +
@@ -471,8 +509,6 @@ distribute - ${project.build.directory}/shared-resources - true true @@ -612,37 +648,6 @@ - - - - org.apache.maven.plugins - maven-javadoc-plugin - - true -
${project.name}
- ${java.version} - true - ${shared.resources}/javadoc - - ${shared.resources}/javadoc/overview.html - - - ${shared.resources}/javadoc/spring-javadoc.css - - - true - -Xdoclint:none - - - http://docs.spring.io/spring/docs/current/javadoc-api/ - - http://docs.oracle.com/javase/6/docs/api - -
-
- diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index 3f9a201d..13d24584 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -20,6 +22,20 @@ META-INF + + + + maven-jar-plugin + + + test-jar + + test-jar + + + + + diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java index e8cb5ad0..a32ceb87 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AwsEc2Authentication.java @@ -53,7 +53,7 @@ public class AwsEc2Authentication implements ClientAuthentication, private static final Log logger = LogFactory.getLog(AwsEc2Authentication.class); - private final static char[] EMPTY = new char[0]; + private static final char[] EMPTY = new char[0]; private final AwsEc2AuthenticationOptions options; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java index b8088a3c..a2f010bf 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CachingVaultTokenSupplier.java @@ -34,7 +34,7 @@ import org.springframework.vault.support.VaultToken; */ public class CachingVaultTokenSupplier implements VaultTokenSupplier { - private final static Mono EMPTY = Mono.empty(); + private static final Mono EMPTY = Mono.empty(); private final VaultTokenSupplier clientAuthentication; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java b/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java index 9f85fd43..7a7339ab 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/SslConfiguration.java @@ -40,7 +40,7 @@ import org.springframework.util.Assert; */ public class SslConfiguration { - private final static String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType(); + private static final String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType(); private final KeyStoreConfiguration keyStoreConfiguration; diff --git a/spring-vault-repository/pom.xml b/spring-vault-repository/pom.xml new file mode 100644 index 00000000..333fc28e --- /dev/null +++ b/spring-vault-repository/pom.xml @@ -0,0 +1,136 @@ + + + 4.0.0 + + + org.springframework.vault + spring-vault-parent + 2.0.0.BUILD-SNAPSHOT + + + spring-vault-repository + Spring Vault Repository + Spring Vault Repository support + jar + + + + + ../src/main/resources + META-INF + + + + + + + org.springframework.vault + spring-vault-core + + + + org.springframework.vault + spring-vault-core + ${project.version} + test-jar + + + + org.springframework + spring-core + + + + org.springframework + spring-context + + + + org.springframework + spring-beans + + + + org.springframework + spring-web + + + + org.springframework.data + spring-data-commons + + + + org.springframework.data + spring-data-keyvalue + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.apache.httpcomponents + httpclient + true + + + + org.apache.httpcomponents + httpcore + true + + + + org.projectlombok + lombok + 1.16.10 + true + + + + + + org.springframework + spring-test + test + + + + org.assertj + assertj-core + test + + + + com.jayway.jsonpath + json-path + test + + + + junit + junit + test + + + + org.mockito + mockito-core + test + + + + + + ch.qos.logback + logback-classic + 1.2.2 + test + + + + diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/EnableVaultRepositories.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/EnableVaultRepositories.java new file mode 100644 index 00000000..f6884d0a --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/EnableVaultRepositories.java @@ -0,0 +1,136 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.config.QueryCreatorType; +import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.vault.repository.query.VaultQueryCreator; +import org.springframework.vault.repository.support.VaultRepositoryFactoryBean; + +/** + * Annotation to activate Vault repositories. If no base package is configured through + * either {@link #value()}, {@link #basePackages()} or {@link #basePackageClasses()} it + * will trigger scanning of the package of annotated class. + * + * @author Mark Paluch + * @since 2.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(VaultRepositoriesRegistrar.class) +@QueryCreatorType(VaultQueryCreator.class) +public @interface EnableVaultRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation + * declarations e.g.: {@code @EnableVaultRepositories("org.my.pkg")} instead of + * {@code @EnableVaultRepositories(basePackages="org.my.pkg")}. + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. {@link #value()} is an alias for + * (and mutually exclusive with) this attribute. Use {@link #basePackageClasses()} for + * a type-safe alternative to String-based package names. + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to + * scan for annotated components. The package of each class specified will be scanned. + * Consider creating a special no-op marker class or interface in each package that + * serves no purpose other than being referenced by this attribute. + */ + Class[] basePackageClasses() default {}; + + /** + * Specifies which types are not eligible for component scanning. + */ + Filter[] excludeFilters() default {}; + + /** + * Specifies which types are eligible for component scanning. Further narrows the set + * of candidate components from everything in {@link #basePackages()} to everything in + * the base packages that matches the given filter or filters. + */ + Filter[] includeFilters() default {}; + + /** + * Returns the postfix to be used when looking up custom repository implementations. + * Defaults to {@literal Impl}. So for a repository named {@code PersonRepository} the + * corresponding implementation class will be looked up scanning for + * {@code PersonRepositoryImpl}. + */ + String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures the location of where to find the Spring Data named queries properties + * file. + */ + String namedQueriesLocation() default ""; + + /** + * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries + * for query methods. Defaults to {@link Key#CREATE_IF_NOT_FOUND}. + */ + Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; + + /** + * Returns the {@link FactoryBean} class to be used for each repository instance. + * Defaults to {@link VaultRepositoryFactoryBean}. + */ + Class repositoryFactoryBeanClass() default VaultRepositoryFactoryBean.class; + + /** + * Configure the repository base class to be used to create repository proxies for + * this particular configuration. + */ + Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; + + /** + * Configures the name of the {@link KeyValueOperations} bean to be used with the + * repositories detected. + */ + String keyValueTemplateRef() default "vaultKeyValueTemplate"; + + /** + * Configures whether nested repository-interfaces (e.g. defined as inner classes) + * should be discovered by the repositories infrastructure. + */ + boolean considerNestedRepositories() default false; + + /** + * Configures the bean name of the + * {@link org.springframework.vault.core.VaultOperations} to be used. Defaulted to + * {@literal vaultTemplate}. + */ + String vaultTemplateRef() default "vaultTemplate"; +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoriesRegistrar.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoriesRegistrar.java new file mode 100644 index 00000000..e6b8bc7c --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoriesRegistrar.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.configuration; + +import java.lang.annotation.Annotation; + +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * Vault specific {@link ImportBeanDefinitionRegistrar}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + @Override + protected Class getAnnotation() { + return EnableVaultRepositories.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new VaultRepositoryConfigurationExtension(); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoryConfigurationExtension.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoryConfigurationExtension.java new file mode 100644 index 00000000..4e55c028 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/configuration/VaultRepositoryConfigurationExtension.java @@ -0,0 +1,217 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.configuration; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; +import org.springframework.data.keyvalue.repository.config.QueryCreatorType; +import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; +import org.springframework.data.keyvalue.repository.query.SpelQueryCreator; +import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.vault.repository.core.VaultKeyValueAdapter; +import org.springframework.vault.repository.core.VaultKeyValueTemplate; +import org.springframework.vault.repository.mapping.Secret; +import org.springframework.vault.repository.mapping.VaultMappingContext; + +/** + * {@link RepositoryConfigurationExtension} for Vault. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultRepositoryConfigurationExtension extends + KeyValueRepositoryConfigurationExtension { + + private static final String VAULT_ADAPTER_BEAN_NAME = "vaultKeyValueAdapter"; + + private static final String VAULT_MAPPING_CONTEXT_BEAN_NAME = "vaultMappingContext"; + + @Override + public String getModuleName() { + return "Vault"; + } + + @Override + protected String getModulePrefix() { + return "vault"; + } + + @Override + protected String getDefaultKeyValueTemplateRef() { + return "vaultKeyValueTemplate"; + } + + // TODO: Remove this as soon as KeyValue provides overrides for the mapping context + // name. + @Override + public void postProcess(BeanDefinitionBuilder builder, + AnnotationRepositoryConfigurationSource config) { + + AnnotationAttributes attributes = config.getAttributes(); + + builder.addPropertyReference("keyValueOperations", + attributes.getString(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE)); + builder.addPropertyValue("queryCreator", getQueryCreatorType(config)); + builder.addPropertyValue("queryType", getQueryType(config)); + builder.addPropertyReference("mappingContext", VAULT_MAPPING_CONTEXT_BEAN_NAME); + } + + /** + * Detects the query creator type to be used for the factory to set. Will lookup a + * {@link QueryCreatorType} annotation on the {@code @Enable}-annotation or use + * {@link SpelQueryCreator} if not found. + * + * @param config + * @return + */ + private static Class getQueryCreatorType( + AnnotationRepositoryConfigurationSource config) { + + AnnotationMetadata metadata = config.getEnableAnnotationMetadata(); + + Map queryCreatorAnnotationAttributes = metadata + .getAnnotationAttributes(QueryCreatorType.class.getName()); + + if (queryCreatorAnnotationAttributes == null) { + return SpelQueryCreator.class; + } + + AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes( + queryCreatorAnnotationAttributes); + return queryCreatorAttributes.getClass("value"); + } + + /** + * Detects the query creator type to be used for the factory to set. Will lookup a + * {@link QueryCreatorType} annotation on the {@code @Enable}-annotation or use + * {@link SpelQueryCreator} if not found. + * + * @param config + * @return + */ + private static Class getQueryType(AnnotationRepositoryConfigurationSource config) { + + AnnotationMetadata metadata = config.getEnableAnnotationMetadata(); + + Map queryCreatorAnnotationAttributes = metadata + .getAnnotationAttributes(QueryCreatorType.class.getName()); + + if (queryCreatorAnnotationAttributes == null) { + return KeyValuePartTreeQuery.class; + } + + AnnotationAttributes queryCreatorAttributes = new AnnotationAttributes( + queryCreatorAnnotationAttributes); + return queryCreatorAttributes.getClass("repositoryQueryType"); + } + + @Override + public void registerBeansForRoot(BeanDefinitionRegistry registry, + RepositoryConfigurationSource configurationSource) { + + Optional vaultTemplateRef = configurationSource + .getAttribute("vaultTemplateRef"); + + RootBeanDefinition mappingContextDefinition = createVaultMappingContext(configurationSource); + mappingContextDefinition.setSource(configurationSource.getSource()); + + registerIfNotAlreadyRegistered(mappingContextDefinition, registry, + VAULT_MAPPING_CONTEXT_BEAN_NAME, configurationSource); + + // register Adapter + RootBeanDefinition vaultKeyValueAdapterDefinition = new RootBeanDefinition( + VaultKeyValueAdapter.class); + + ConstructorArgumentValues constructorArgumentValuesForVaultKeyValueAdapter = new ConstructorArgumentValues(); + + constructorArgumentValuesForVaultKeyValueAdapter.addIndexedArgumentValue(0, + new RuntimeBeanReference(vaultTemplateRef.orElse("vaultTemplate"))); + + vaultKeyValueAdapterDefinition + .setConstructorArgumentValues(constructorArgumentValuesForVaultKeyValueAdapter); + + registerIfNotAlreadyRegistered(vaultKeyValueAdapterDefinition, registry, + VAULT_ADAPTER_BEAN_NAME, configurationSource); + + Optional keyValueTemplateName = configurationSource + .getAttribute(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE); + + // No custom template reference configured and no matching bean definition found + if (keyValueTemplateName.isPresent() + && getDefaultKeyValueTemplateRef().equals(keyValueTemplateName.get()) + && !registry.containsBeanDefinition(keyValueTemplateName.get())) { + + AbstractBeanDefinition beanDefinition = getDefaultKeyValueTemplateBeanDefinition(configurationSource); + + if (beanDefinition != null) { + registerIfNotAlreadyRegistered(beanDefinition, registry, + keyValueTemplateName.get(), configurationSource.getSource()); + } + } + } + + private RootBeanDefinition createVaultMappingContext( + RepositoryConfigurationSource configurationSource) { + + ConstructorArgumentValues mappingContextArgs = new ConstructorArgumentValues(); + + RootBeanDefinition mappingContextBeanDef = new RootBeanDefinition( + VaultMappingContext.class); + mappingContextBeanDef.setConstructorArgumentValues(mappingContextArgs); + + return mappingContextBeanDef; + } + + @Override + protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition( + RepositoryConfigurationSource configurationSource) { + + RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition( + VaultKeyValueTemplate.class); + + ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues(); + constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(0, + new RuntimeBeanReference(VAULT_ADAPTER_BEAN_NAME)); + + constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(1, + new RuntimeBeanReference(VAULT_MAPPING_CONTEXT_BEAN_NAME)); + + keyValueTemplateDefinition + .setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate); + + return keyValueTemplateDefinition; + } + + @Override + protected Collection> getIdentifyingAnnotations() { + return Collections.> singleton(Secret.class); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/AbstractVaultConverter.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/AbstractVaultConverter.java new file mode 100644 index 00000000..4c9e0a3d --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/AbstractVaultConverter.java @@ -0,0 +1,84 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.EntityInstantiators; + +/** + * Base class for {@link VaultConverter} implementations. Sets up a + * {@link GenericConversionService} and populates basic converters. + * + * @author Mark Paluch + * @since 2.0 + */ +public abstract class AbstractVaultConverter implements VaultConverter, InitializingBean { + + protected final GenericConversionService conversionService; + + protected CustomConversions conversions = new VaultCustomConversions(); + + protected EntityInstantiators instantiators = new EntityInstantiators(); + + /** + * Creates a new {@link AbstractVaultConverter} using the given + * {@link GenericConversionService}. + * + * @param conversionService must not be {@literal null}. + */ + public AbstractVaultConverter(GenericConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Registers the given custom conversions with the converter. + * + * @param conversions + */ + public void setCustomConversions(CustomConversions conversions) { + this.conversions = conversions; + } + + /** + * Registers {@link EntityInstantiators} to customize entity instantiation. + * + * @param instantiators + */ + public void setInstantiators(EntityInstantiators instantiators) { + this.instantiators = instantiators; + } + + @Override + public ConversionService getConversionService() { + return conversionService; + } + + @Override + public void afterPropertiesSet() { + initializeConverters(); + } + + /** + * Registers additional converters that will be available when using the + * {@link ConversionService} directly (e.g. for id conversion). + */ + private void initializeConverters() { + conversions.registerConvertersIn(conversionService); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapper.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapper.java new file mode 100644 index 00000000..092f8c86 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapper.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.data.convert.DefaultTypeMapper; +import org.springframework.data.convert.SimpleTypeInformationMapper; +import org.springframework.data.convert.TypeAliasAccessor; +import org.springframework.data.convert.TypeInformationMapper; +import org.springframework.data.mapping.Alias; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * Default implementation of {@link VaultTypeMapper} allowing configuration of the key to + * lookup and store type information in {@link SecretDocument}. The key defaults to + * {@link #DEFAULT_TYPE_KEY}. Actual type-to-{@link String} conversion and back is done in + * {@link #readType(Object)} or {@link #getDefaultedTypeToBeUsed(Object)}. respectively. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultVaultTypeMapper extends DefaultTypeMapper> + implements VaultTypeMapper { + + public static final String DEFAULT_TYPE_KEY = "_class"; + + @SuppressWarnings("rawtypes") + private static final TypeInformation MAP_TYPE_INFO = ClassTypeInformation + .from(Map.class); + + private final @Nullable String typeKey; + + /** + * Creates a default {@link VaultTypeMapper} that exchanges types using the type key + * {@literal _class}. + */ + public DefaultVaultTypeMapper() { + this(DEFAULT_TYPE_KEY); + } + + /** + * Creates a default {@link VaultTypeMapper} that exchanges types using the given + * {@code typeKey}. + * + * @param typeKey may not be {@literal null} to disable type hinting. + */ + public DefaultVaultTypeMapper(@Nullable String typeKey) { + this(typeKey, Collections.singletonList(new SimpleTypeInformationMapper())); + } + + /** + * Creates a default {@link VaultTypeMapper} that exchanges types using the given + * {@code typeKey} and {@link MappingContext}. + * + * @param typeKey may not be {@literal null} to disable type hinting. + * @param mappingContext must not be {@literal null} or empty. + */ + public DefaultVaultTypeMapper(@Nullable String typeKey, + MappingContext, ?> mappingContext) { + this(typeKey, new SecretDocumentTypeAliasAccessor(typeKey), mappingContext, + Collections.singletonList(new SimpleTypeInformationMapper())); + } + + public DefaultVaultTypeMapper(@Nullable String typeKey, + List mappers) { + this(typeKey, new SecretDocumentTypeAliasAccessor(typeKey), null, mappers); + } + + private DefaultVaultTypeMapper(@Nullable String typeKey, + TypeAliasAccessor> accessor, + MappingContext, ?> mappingContext, + List mappers) { + + super(accessor, mappingContext, mappers); + + this.typeKey = typeKey; + } + + /** + * Checks whether the given key name matches the {@literal typeKey}. + * + * @param key + * @return {@literal true} if {@code key} matches the {@literal typeKey}. + */ + public boolean isTypeKey(String key) { + return typeKey != null && typeKey.equals(key); + } + + @Override + protected TypeInformation getFallbackTypeFor(Map source) { + return MAP_TYPE_INFO; + } + + /** + * {@link TypeAliasAccessor} to store aliases in a {@link SecretDocument}. + * + * @author Mark Paluch + */ + static class SecretDocumentTypeAliasAccessor implements + TypeAliasAccessor> { + + private final @Nullable String typeKey; + + SecretDocumentTypeAliasAccessor(@Nullable String typeKey) { + this.typeKey = typeKey; + } + + public Alias readAliasFrom(Map source) { + return typeKey == null ? Alias.NONE : Alias.ofNullable(source.get(typeKey)); + } + + public void writeTypeTo(Map sink, Object alias) { + if (typeKey != null) { + sink.put(typeKey, alias); + } + } + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/MappingVaultConverter.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/MappingVaultConverter.java new file mode 100644 index 00000000..175e3155 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/MappingVaultConverter.java @@ -0,0 +1,757 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Map.Entry; + +import org.springframework.core.CollectionFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.vault.repository.mapping.VaultPersistentEntity; +import org.springframework.vault.repository.mapping.VaultPersistentProperty; + +/** + * {@link VaultConverter} that uses a {@link MappingContext} to do sophisticated mapping + * of domain objects to {@link SecretDocument}. This converter converts between Map-typed + * representations and domain objects without use of a JSON library. + * {@link SecretDocument} is the input to JSON mapping to exchange secrets with Vault. + * + * @author Mark Paluch + * @since 2.0 + */ +public class MappingVaultConverter extends AbstractVaultConverter { + + private final MappingContext, VaultPersistentProperty> mappingContext; + + private VaultTypeMapper typeMapper; + + public MappingVaultConverter( + MappingContext, VaultPersistentProperty> mappingContext) { + + super(new DefaultConversionService()); + + Assert.notNull(mappingContext, "MappingContext must not be null"); + + this.mappingContext = mappingContext; + this.typeMapper = new DefaultVaultTypeMapper( + DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, mappingContext); + } + + /** + * Configures the {@link VaultTypeMapper} to be used to add type information to + * {@link SecretDocument}s created by the converter and how to lookup type information + * from {@link SecretDocument}s when reading them. Uses a + * {@link DefaultVaultTypeMapper} by default. Setting this to {@literal null} will + * reset the {@link org.springframework.data.convert.TypeMapper} to the default one. + * + * @param typeMapper the typeMapper to set, must not be {@literal null}. + */ + public void setTypeMapper(VaultTypeMapper typeMapper) { + + Assert.notNull(typeMapper, "VaultTypeMapper must not be null"); + this.typeMapper = typeMapper; + } + + @Override + public MappingContext, VaultPersistentProperty> getMappingContext() { + return mappingContext; + } + + @Override + public S read(Class type, SecretDocument source) { + return read(ClassTypeInformation.from(type), source); + } + + @SuppressWarnings("unchecked") + private S read(TypeInformation type, Object source) { + + SecretDocument secretDocument = getSecretDocument(source); + + TypeInformation typeToUse = secretDocument != null ? typeMapper + .readType(secretDocument.getBody(), type) + : (TypeInformation) ClassTypeInformation.OBJECT; + Class rawType = typeToUse.getType(); + + if (conversions.hasCustomReadTarget(source.getClass(), rawType)) { + return conversionService.convert(source, rawType); + } + + if (SecretDocument.class.isAssignableFrom(rawType)) { + return (S) source; + } + + if (Map.class.isAssignableFrom(rawType) && secretDocument != null) { + return (S) secretDocument.getBody(); + } + + if (typeToUse.isMap() && secretDocument != null) { + return (S) readMap(typeToUse, secretDocument.getBody()); + } + + if (typeToUse.equals(ClassTypeInformation.OBJECT)) { + return (S) source; + } + + return read( + (VaultPersistentEntity) mappingContext + .getRequiredPersistentEntity(typeToUse), + secretDocument); + } + + @Nullable + @SuppressWarnings("unchecked") + private SecretDocument getSecretDocument(Object source) { + + SecretDocument secretDocument = null; + if (source instanceof Map) { + secretDocument = new SecretDocument((Map) source); + } + else if (source instanceof SecretDocument) { + secretDocument = (SecretDocument) source; + } + return secretDocument; + } + + private ParameterValueProvider getParameterProvider( + VaultPersistentEntity entity, SecretDocument source) { + + VaultPropertyValueProvider provider = new VaultPropertyValueProvider(source); + + PersistentEntityParameterValueProvider parameterProvider = new PersistentEntityParameterValueProvider<>( + entity, provider, source); + + return new ParameterValueProvider() { + + @Nullable + @Override + public T getParameterValue(Parameter parameter) { + + Object value = parameterProvider.getParameterValue(parameter); + return value != null ? readValue(value, parameter.getType()) : null; + } + }; + } + + private S read(VaultPersistentEntity entity, SecretDocument source) { + + ParameterValueProvider provider = getParameterProvider( + entity, source); + EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); + S instance = instantiator.createInstance(entity, provider); + + PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor( + entity.getPropertyAccessor(instance), conversionService); + + VaultPersistentProperty idProperty = entity.getIdProperty(); + SecretDocumentAccessor documentAccessor = new SecretDocumentAccessor(source); + + // make sure id property is set before all other properties + Object idValue; + + if (idProperty != null && documentAccessor.hasValue(idProperty)) { + + idValue = readIdValue(idProperty, documentAccessor); + accessor.setProperty(idProperty, idValue); + } + + VaultPropertyValueProvider valueProvider = new VaultPropertyValueProvider( + documentAccessor); + + readProperties(entity, accessor, idProperty, documentAccessor, valueProvider); + + return instance; + } + + @Nullable + private Object readIdValue(VaultPersistentProperty idProperty, + SecretDocumentAccessor documentAccessor) { + + Object resolvedValue = documentAccessor.get(idProperty); + + return resolvedValue != null ? readValue(resolvedValue, + idProperty.getTypeInformation()) : null; + } + + private void readProperties(VaultPersistentEntity entity, + PersistentPropertyAccessor accessor, + @Nullable VaultPersistentProperty idProperty, + SecretDocumentAccessor documentAccessor, + VaultPropertyValueProvider valueProvider) { + + for (VaultPersistentProperty prop : entity) { + + // we skip the id property since it was already set + if (idProperty != null && idProperty.equals(prop)) { + continue; + } + + if (entity.isConstructorArgument(prop) || !documentAccessor.hasValue(prop)) { + continue; + } + + accessor.setProperty(prop, valueProvider.getPropertyValue(prop)); + } + } + + @Nullable + @SuppressWarnings("unchecked") + private T readValue(Object value, TypeInformation type) { + + Class rawType = type.getType(); + + if (conversions.hasCustomReadTarget(value.getClass(), rawType)) { + return (T) conversionService.convert(value, rawType); + } + else if (value instanceof List) { + return (T) readCollectionOrArray(type, (List) value); + } + else if (value instanceof Map) { + return (T) read(type, (Map) value); + } + else { + return (T) getPotentiallyConvertedSimpleRead(value, rawType); + } + } + + /** + * Reads the given {@link List} into a collection of the given {@link TypeInformation} + * . + * + * @param targetType must not be {@literal null}. + * @param sourceValue must not be {@literal null}. + * @return the converted {@link Collection} or array, will never be {@literal null}. + */ + @Nullable + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object readCollectionOrArray(TypeInformation targetType, List sourceValue) { + + Assert.notNull(targetType, "Target type must not be null!"); + + Class collectionType = targetType.getType(); + + TypeInformation componentType = targetType.getComponentType() != null ? targetType + .getComponentType() : ClassTypeInformation.OBJECT; + Class rawComponentType = componentType.getType(); + + collectionType = Collection.class.isAssignableFrom(collectionType) ? collectionType + : List.class; + Collection items = targetType.getType().isArray() ? new ArrayList<>( + sourceValue.size()) : CollectionFactory.createCollection(collectionType, + rawComponentType, sourceValue.size()); + + if (sourceValue.isEmpty()) { + return getPotentiallyConvertedSimpleRead(items, collectionType); + } + + for (Object obj : sourceValue) { + + if (obj instanceof Map) { + items.add(read(componentType, (Map) obj)); + } + else if (obj instanceof List) { + items.add(readCollectionOrArray(ClassTypeInformation.OBJECT, (List) obj)); + } + else { + items.add(getPotentiallyConvertedSimpleRead(obj, rawComponentType)); + } + } + + return getPotentiallyConvertedSimpleRead(items, targetType.getType()); + } + + /** + * Reads the given {@link Map} into a {@link Map}. will recursively resolve nested + * {@link Map}s as well. + * + * @param type the {@link Map} {@link TypeInformation} to be used to unmarshall this + * {@link Map}. + * @param sourceMap must not be {@literal null} + * @return + */ + @SuppressWarnings("unchecked") + protected Map readMap(TypeInformation type, + Map sourceMap) { + + Assert.notNull(sourceMap, "Source map must not be null!"); + + Class mapType = typeMapper.readType(sourceMap, type).getType(); + + TypeInformation keyType = type.getComponentType(); + TypeInformation valueType = type.getMapValueType(); + + Class rawKeyType = keyType != null ? keyType.getType() : null; + Class rawValueType = valueType != null ? valueType.getType() : null; + + Map map = CollectionFactory.createMap(mapType, rawKeyType, + sourceMap.keySet().size()); + + for (Entry entry : sourceMap.entrySet()) { + + if (typeMapper.isTypeKey(entry.getKey())) { + continue; + } + + Object key = entry.getKey(); + + if (rawKeyType != null && !rawKeyType.isAssignableFrom(key.getClass())) { + key = conversionService.convert(key, rawKeyType); + } + + Object value = entry.getValue(); + TypeInformation defaultedValueType = valueType != null ? valueType + : ClassTypeInformation.OBJECT; + + if (value instanceof Map) { + map.put(key, read(defaultedValueType, (Map) value)); + } + else if (value instanceof List) { + map.put(key, + readCollectionOrArray(valueType != null ? valueType + : ClassTypeInformation.LIST, (List) value)); + } + else { + map.put(key, getPotentiallyConvertedSimpleRead(value, rawValueType)); + } + } + + return map; + } + + /** + * Checks whether we have a custom conversion for the given simple object. Converts + * the given value if so, applies {@link Enum} handling or returns the value as is. + * + * @param value + * @param target must not be {@literal null}. + * @return + */ + @Nullable + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, + @Nullable Class target) { + + if (value == null || target == null || target.isAssignableFrom(value.getClass())) { + return value; + } + + if (Enum.class.isAssignableFrom(target)) { + return Enum.valueOf((Class) target, value.toString()); + } + + return conversionService.convert(value, target); + } + + @Override + public void write(Object source, SecretDocument sink) { + + Class entityType = ClassUtils.getUserClass(source.getClass()); + TypeInformation type = ClassTypeInformation.from(entityType); + + SecretDocumentAccessor documentAccessor = new SecretDocumentAccessor(sink); + + writeInternal(source, documentAccessor, type); + + boolean handledByCustomConverter = conversions.hasCustomWriteTarget(entityType, + SecretDocument.class); + if (!handledByCustomConverter) { + typeMapper.writeType(type, sink.getBody()); + } + } + + /** + * Internal write conversion method which should be used for nested invocations. + * + * @param obj + * @param sink + * @param typeHint + */ + @SuppressWarnings("unchecked") + protected void writeInternal(Object obj, SecretDocumentAccessor sink, + @Nullable TypeInformation typeHint) { + + Class entityType = obj.getClass(); + Optional> customTarget = conversions.getCustomWriteTarget(entityType, + SecretDocument.class); + + if (customTarget.isPresent()) { + + SecretDocument result = conversionService.convert(obj, SecretDocument.class); + + if (result.getId() != null) { + sink.setId(result.getId()); + } + + sink.getBody().putAll(result.getBody()); + return; + } + + if (Map.class.isAssignableFrom(entityType)) { + writeMapInternal((Map) obj, sink.getBody(), + ClassTypeInformation.MAP); + return; + } + + VaultPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(entityType); + writeInternal(obj, sink, entity); + addCustomTypeKeyIfNecessary(typeHint, obj, sink); + } + + protected void writeInternal(Object obj, SecretDocumentAccessor sink, + VaultPersistentEntity entity) { + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(obj); + + VaultPersistentProperty idProperty = entity.getIdProperty(); + if (idProperty != null && !sink.hasValue(idProperty)) { + + Object value = accessor.getProperty(idProperty); + + if (value != null) { + sink.put(idProperty, value); + } + } + writeProperties(entity, accessor, sink, idProperty); + } + + private void writeProperties(VaultPersistentEntity entity, + PersistentPropertyAccessor accessor, SecretDocumentAccessor sink, + @Nullable VaultPersistentProperty idProperty) { + + // Write the properties + for (VaultPersistentProperty prop : entity) { + + if (prop.equals(idProperty) || !prop.isWritable()) { + continue; + } + + Object value = accessor.getProperty(prop); + + if (value == null) { + continue; + } + + if (!conversions.isSimpleType(value.getClass())) { + writePropertyInternal(value, sink, prop); + } + else { + + sink.put(prop, getPotentiallyConvertedSimpleWrite(value)); + } + } + } + + @SuppressWarnings({ "unchecked" }) + protected void writePropertyInternal(@Nullable Object obj, + SecretDocumentAccessor accessor, VaultPersistentProperty prop) { + + if (obj == null) { + return; + } + + TypeInformation valueType = ClassTypeInformation.from(obj.getClass()); + TypeInformation type = prop.getTypeInformation(); + + if (valueType.isCollectionLike()) { + List collectionInternal = createCollection(asCollection(obj), prop); + accessor.put(prop, collectionInternal); + return; + } + + if (valueType.isMap()) { + Map mapDbObj = createMap((Map) obj, prop); + accessor.put(prop, mapDbObj); + return; + } + + // Lookup potential custom target type + Optional> basicTargetType = conversions.getCustomWriteTarget(obj + .getClass()); + + if (basicTargetType.isPresent()) { + + accessor.put(prop, conversionService.convert(obj, basicTargetType.get())); + return; + } + + VaultPersistentEntity entity = isSubtype(prop.getType(), obj.getClass()) ? mappingContext + .getRequiredPersistentEntity(obj.getClass()) : mappingContext + .getRequiredPersistentEntity(type); + + SecretDocumentAccessor nested = accessor.writeNested(prop); + + writeInternal(obj, nested, entity); + addCustomTypeKeyIfNecessary(ClassTypeInformation.from(prop.getRawType()), obj, + nested); + } + + private static boolean isSubtype(Class left, Class right) { + return left.isAssignableFrom(right) && !left.equals(right); + } + + /** + * Writes the given {@link Collection} using the given {@link VaultPersistentProperty} + * information. + * + * @param collection must not be {@literal null}. + * @param property must not be {@literal null}. + * @return + */ + protected List createCollection(Collection collection, + VaultPersistentProperty property) { + + return writeCollectionInternal(collection, property.getTypeInformation(), + new ArrayList<>()); + } + + /** + * Populates the given {@link List} with values from the given {@link Collection}. + * + * @param source the collection to create a {@link List} for, must not be + * {@literal null}. + * @param type the {@link TypeInformation} to consider or {@literal null} if unknown. + * @param sink the {@link List} to write to. + * @return + */ + private List writeCollectionInternal(Collection source, + @Nullable TypeInformation type, List sink) { + + TypeInformation componentType = null; + + if (type != null) { + componentType = type.getComponentType(); + } + + for (Object element : source) { + + Class elementType = element == null ? null : element.getClass(); + + if (elementType == null || conversions.isSimpleType(elementType)) { + sink.add(getPotentiallyConvertedSimpleWrite(element)); + } + else if (element instanceof Collection || elementType.isArray()) { + sink.add(writeCollectionInternal(asCollection(element), componentType, + new ArrayList<>())); + } + else { + SecretDocumentAccessor accessor = new SecretDocumentAccessor( + new SecretDocument()); + writeInternal(element, accessor, componentType); + sink.add(accessor.getBody()); + } + } + + return sink; + } + + /** + * Writes the given {@link Map} using the given {@link VaultPersistentProperty} + * information. + * + * @param map must not {@literal null}. + * @param property must not be {@literal null}. + * @return + */ + protected Map createMap(Map map, + VaultPersistentProperty property) { + + Assert.notNull(map, "Given map must not be null!"); + Assert.notNull(property, "PersistentProperty must not be null!"); + + return writeMapInternal(map, new LinkedHashMap<>(), property.getTypeInformation()); + } + + /** + * Writes the given {@link Map} to the given {@link Map} considering the given + * {@link TypeInformation}. + * + * @param obj must not be {@literal null}. + * @param bson must not be {@literal null}. + * @param propertyType must not be {@literal null}. + * @return + */ + protected Map writeMapInternal(Map obj, + Map bson, TypeInformation propertyType) { + + for (Map.Entry entry : obj.entrySet()) { + + Object key = entry.getKey(); + Object val = entry.getValue(); + + if (conversions.isSimpleType(key.getClass())) { + + String simpleKey = key.toString(); + if (val == null || conversions.isSimpleType(val.getClass())) { + bson.put(simpleKey, val); + } + else if (val instanceof Collection || val.getClass().isArray()) { + + bson.put( + simpleKey, + writeCollectionInternal(asCollection(val), + propertyType.getMapValueType(), new ArrayList<>())); + } + else { + SecretDocumentAccessor nested = new SecretDocumentAccessor( + new SecretDocument()); + TypeInformation valueTypeInfo = propertyType.isMap() ? propertyType + .getMapValueType() : ClassTypeInformation.OBJECT; + writeInternal(val, nested, valueTypeInfo); + bson.put(simpleKey, nested.getBody()); + } + } + else { + throw new MappingException("Cannot use a complex object as a key value."); + } + } + + return bson; + } + + /** + * Adds custom type information to the given {@link SecretDocument} if necessary. That + * is if the value is not the same as the one given. This is usually the case if you + * store a subtype of the actual declared type of the property. + * + * @param type + * @param value must not be {@literal null}. + * @param accessor must not be {@literal null}. + */ + protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation type, + Object value, SecretDocumentAccessor accessor) { + + Class reference = type != null ? type.getActualType().getType() : Object.class; + Class valueType = ClassUtils.getUserClass(value.getClass()); + + boolean notTheSameClass = !valueType.equals(reference); + if (notTheSameClass) { + typeMapper.writeType(valueType, accessor.getBody()); + } + } + + /** + * Checks whether we have a custom conversion registered for the given value into an + * arbitrary simple Vault type. Returns the converted value if so. If not, we perform + * special enum handling or simply return the value as is. + * + * @param value + * @return + */ + @Nullable + private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) { + + if (value == null) { + return null; + } + + Optional> customTarget = conversions.getCustomWriteTarget(value + .getClass()); + + if (customTarget.isPresent()) { + return conversionService.convert(value, customTarget.get()); + } + + if (ObjectUtils.isArray(value)) { + + if (value instanceof byte[]) { + return value; + } + return asCollection(value); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() + : value; + } + + /** + * Returns given object as {@link Collection}. Will return the {@link Collection} as + * is if the source is a {@link Collection} already, will convert an array into a + * {@link Collection} or simply create a single element collection for everything + * else. + * + * @param source + * @return + */ + private static Collection asCollection(Object source) { + + if (source instanceof Collection) { + return (Collection) source; + } + + return source.getClass().isArray() ? CollectionUtils.arrayToList(source) + : Collections.singleton(source); + } + + /** + * {@link PropertyValueProvider} to evaluate a SpEL expression if present on the + * property or simply accesses the field of the configured source + * {@link SecretDocument}. + * + */ + class VaultPropertyValueProvider implements + PropertyValueProvider { + + private final SecretDocumentAccessor source; + + VaultPropertyValueProvider(SecretDocument source) { + + Assert.notNull(source, "Source document must no be null!"); + + this.source = new SecretDocumentAccessor(source); + } + + VaultPropertyValueProvider(SecretDocumentAccessor accessor) { + + Assert.notNull(accessor, "SecretDocumentAccessor must no be null!"); + + this.source = accessor; + } + + @Nullable + public T getPropertyValue(VaultPersistentProperty property) { + + Object value = source.get(property); + + if (value == null) { + return null; + } + + return readValue(value, property.getTypeInformation()); + } + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java new file mode 100644 index 00000000..7a338007 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocument.java @@ -0,0 +1,137 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.LinkedHashMap; +import java.util.Map; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.vault.support.VaultResponse; + +/** + * Vault database exchange object containing data before/after it's exchanged with Vault. + * A {@link SecretDocument} is basically an object with an {@code id} and a body + * represented as {@link Map} of {@link String} and {@link Object}. It can be created + * {@link #from(String, VaultResponse) from} an Id and {@link VaultResponse}. + *

+ * A secret document can hold simple properties, {@link java.util.Collection list} + * properties and nested objects as {@link Map}s. + * + * @author Mark Paluch + * @since 2.0 + */ +@EqualsAndHashCode +@ToString +public class SecretDocument { + + private @Nullable String id; + + private final Map body; + + /** + * Create a new, empty {@link SecretDocument}. + */ + public SecretDocument() { + this(null, new LinkedHashMap<>()); + } + + /** + * Create a new {@link SecretDocument} given a {@link Map body map}. + * @param body must not be {@literal null}. + */ + public SecretDocument(Map body) { + this(null, body); + } + + /** + * Create a new {@link SecretDocument} given an {@code id} and {@link Map body map}. + * @param id may be {@literal null}. + * @param body must not be {@literal null}. + */ + public SecretDocument(@Nullable String id, Map body) { + + Assert.notNull(body, "Body must not be null"); + + this.id = id; + this.body = body; + } + + public SecretDocument(String id) { + this(id, new LinkedHashMap<>()); + } + + /** + * Factory method to create a {@link SecretDocument} from an {@code id} and + * {@link VaultResponse}. + * + * @param id must not be {@literal null}. + * @param vaultResponse must not be {@literal null}. + * @return the {@link SecretDocument}. + */ + @SuppressWarnings("ConstantConditions") + public static SecretDocument from(@Nullable String id, VaultResponse vaultResponse) { + return new SecretDocument(id, vaultResponse.getData()); + } + + /** + * @return the Id or {@literal null} if the Id is not set. + */ + @Nullable + public String getId() { + return id; + } + + /** + * Set the Id. + * + * @param id may be {@literal null}. + */ + public void setId(@Nullable String id) { + this.id = id; + } + + /** + * @return the body of this {@link SecretDocument} + */ + public Map getBody() { + return body; + } + + /** + * Retrieve a value from the secret document by its {@code key}. + * + * @param key must not be {@literal null}. + * @return the value or {@literal null}, if the value is not present. + */ + @Nullable + public Object get(String key) { + return body.get(key); + } + + /** + * Set a value in the secret document. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + */ + public void put(String key, Object value) { + this.body.put(key, value); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java new file mode 100644 index 00000000..86b26b7c --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/SecretDocumentAccessor.java @@ -0,0 +1,262 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.vault.repository.mapping.VaultPersistentProperty; + +/** + * Wrapper value object for a {@link SecretDocument} to be able to access raw values by + * {@link VaultPersistentProperty} references. The accessors will transparently resolve + * nested document values that a {@link VaultPersistentProperty} might refer to through a + * path expression in field names. + * + * @author Mark Paluch + * @since 2.0 + */ +class SecretDocumentAccessor { + + private final SecretDocument document; + + private final Map body; + + /** + * Creates a new {@link SecretDocumentAccessor} for the given {@link SecretDocument}. + * + * @param document must be a {@link SecretDocument} effectively, must not be + * {@literal null}. + */ + SecretDocumentAccessor(SecretDocument document) { + + Assert.notNull(document, "SecretDocument must not be null!"); + + this.document = document; + this.body = document.getBody(); + } + + /** + * Creates a new {@link SecretDocumentAccessor} for the given {@link SecretDocument} + * and {@link Map body}. + * + * @param document must be a {@link SecretDocument} effectively, must not be + * {@literal null} + * @param body must not be {@literal null}. + */ + private SecretDocumentAccessor(SecretDocument document, Map body) { + + Assert.notNull(document, "SecretDocument must not be null!"); + Assert.notNull(body, "Body must not be null!"); + + this.document = document; + this.body = body; + } + + /** + * Puts the given value into the backing {@link SecretDocument} based on the + * coordinates defined through the given {@link VaultPersistentProperty}. By default + * this will be the plain field name. But field names might also consist of path + * traversals so we might need to create intermediate {@link Map}s. + * + * @param prop must not be {@literal null}. + * @param value + */ + void put(VaultPersistentProperty prop, @Nullable Object value) { + + Assert.notNull(prop, "VaultPersistentProperty must not be null!"); + String fieldName = prop.getName(); + + if (prop.isIdProperty()) { + this.document.setId((String) value); + return; + } + + if (!fieldName.contains(".")) { + this.body.put(fieldName, value); + return; + } + + Iterator parts = Arrays.asList(fieldName.split("\\.")).iterator(); + Map document = this.body; + + while (parts.hasNext()) { + + String part = parts.next(); + + if (parts.hasNext()) { + document = getOrCreateNestedDocument(part, document); + } + else { + document.put(fieldName, value); + } + } + } + + /** + * Returns the value the given {@link VaultPersistentProperty} refers to. By default + * this will be a direct field but the method will also transparently resolve nested + * values the {@link VaultPersistentProperty} might refer to through a path expression + * in the field name metadata. + * + * @param property must not be {@literal null}. + * @return + */ + @Nullable + Object get(VaultPersistentProperty property) { + + String fieldName = property.getName(); + + if (property.isIdProperty()) { + return this.document.getId(); + } + + if (!fieldName.contains(".")) { + return this.body.get(fieldName); + } + + Iterator parts = Arrays.asList(fieldName.split("\\.")).iterator(); + Map source = this.body; + Object result = null; + + while (source != null && parts.hasNext()) { + + result = source.get(parts.next()); + + if (parts.hasNext()) { + source = getAsMap(result); + } + } + + return result; + } + + /** + * Returns whether the underlying {@link SecretDocument} has a value ({@literal null} + * or non-{@literal null}) for the given {@link VaultPersistentProperty}. + * + * @param property must not be {@literal null}. + * @return + */ + boolean hasValue(VaultPersistentProperty property) { + + Assert.notNull(property, "Property must not be null!"); + + if (property.isIdProperty()) { + return StringUtils.hasText(this.document.getId()); + } + + String fieldName = property.getName(); + + if (!fieldName.contains(".")) { + return this.body.containsKey(fieldName); + } + + String[] parts = fieldName.split("\\."); + Map source = this.body; + + Object result = null; + + for (int i = 1; i < parts.length; i++) { + + result = source.get(parts[i - 1]); + source = getAsMap(result); + + if (source == null) { + return false; + } + } + + return source.containsKey(parts[parts.length - 1]); + } + + /** + * Returns the given source object as map, i.e. maps as is or {@literal null} + * otherwise. + * + * @param source can be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + @Nullable + private static Map getAsMap(Object source) { + + if (source instanceof Map) { + return (Map) source; + } + + return null; + } + + /** + * Returns the {@link Map} which either already exists in the given source under the + * given key, or creates a new nested one, registers it with the source and returns + * it. + * + * @param key must not be {@literal null} or empty. + * @param source must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + private static Map getOrCreateNestedDocument(String key, + Map source) { + + Object existing = source.get(key); + + if (existing instanceof Map) { + return (Map) existing; + } + + Map nested = new LinkedHashMap<>(); + source.put(key, nested); + + return nested; + } + + public Map getBody() { + return body; + } + + public void setId(String id) { + this.document.setId(id); + } + + /** + * Obtains a nested {@link SecretDocumentAccessor} for a + * {@link VaultPersistentProperty}. Nested accessors allows mapping of structured + * hierarchies and represent accessors to nested maps. + * + * @param property must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + public SecretDocumentAccessor writeNested(VaultPersistentProperty property) { + + Map body = (Map) get(property); + + if (body == null) { + body = new LinkedHashMap<>(); + put(property, body); + } + + return new SecretDocumentAccessor(document, body); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java new file mode 100644 index 00000000..9d71aa5c --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultConverter.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import org.springframework.data.convert.EntityConverter; +import org.springframework.vault.repository.mapping.VaultPersistentEntity; +import org.springframework.vault.repository.mapping.VaultPersistentProperty; + +/** + * Central Vault-specific converter interface. + * + * @since 2.0 + */ +public interface VaultConverter + extends + EntityConverter, VaultPersistentProperty, Object, SecretDocument> { +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java new file mode 100644 index 00000000..8b762c2a --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultCustomConversions.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.data.convert.JodaTimeConverters; +import org.springframework.data.convert.WritingConverter; +import org.springframework.vault.repository.mapping.VaultSimpleTypes; + +/** + * Value object to capture custom conversion. {@link VaultCustomConversions} also act as + * factory for {@link org.springframework.data.mapping.model.SimpleTypeHolder} + * + * @author Mark Paluch + * @since 2.0 + * @see org.springframework.data.convert.CustomConversions + * @see org.springframework.data.mapping.model.SimpleTypeHolder + * @see VaultSimpleTypes + */ +public class VaultCustomConversions extends + org.springframework.data.convert.CustomConversions { + + private static final StoreConversions STORE_CONVERSIONS; + private static final List STORE_CONVERTERS; + + static { + + List converters = new ArrayList<>(); + + converters.add(CustomToStringConverter.INSTANCE); + converters.addAll(JodaTimeConverters.getConvertersToRegister()); + + STORE_CONVERTERS = Collections.unmodifiableList(converters); + STORE_CONVERSIONS = StoreConversions + .of(VaultSimpleTypes.HOLDER, STORE_CONVERTERS); + } + + /** + * Creates an empty {@link VaultCustomConversions} object. + */ + VaultCustomConversions() { + this(Collections.emptyList()); + } + + /** + * Create a new {@link VaultCustomConversions} instance registering the given + * converters. + * + * @param converters must not be {@literal null}. + */ + public VaultCustomConversions(List converters) { + super(STORE_CONVERSIONS, converters); + } + + @WritingConverter + private enum CustomToStringConverter implements GenericConverter { + + INSTANCE; + + public Set getConvertibleTypes() { + + ConvertiblePair localeToString = new ConvertiblePair(Locale.class, + String.class); + ConvertiblePair booleanToString = new ConvertiblePair(Character.class, + String.class); + + return new HashSet<>(Arrays.asList(localeToString, booleanToString)); + } + + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return source.toString(); + } + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultTypeMapper.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultTypeMapper.java new file mode 100644 index 00000000..6ac9ed41 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/VaultTypeMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.Map; + +import org.springframework.data.convert.TypeMapper; + +/** + * Vault-specific {@link TypeMapper} exposing that {@link SecretDocument}s might contain a + * type key. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface VaultTypeMapper extends TypeMapper> { + + /** + * Returns whether the given key is the type key. + * + * @return + */ + boolean isTypeKey(String key); +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/package-info.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/package-info.java new file mode 100644 index 00000000..d40313aa --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/convert/package-info.java @@ -0,0 +1,8 @@ +/** + * Spring Vault specific converter infrastructure. + */ +@NonNullApi +package org.springframework.vault.repository.convert; + +import org.springframework.lang.NonNullApi; + diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/MappingVaultEntityInformation.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/MappingVaultEntityInformation.java new file mode 100644 index 00000000..c2a2d3f5 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/MappingVaultEntityInformation.java @@ -0,0 +1,49 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.core; + +import org.springframework.data.mapping.MappingException; +import org.springframework.data.repository.core.support.PersistentEntityInformation; +import org.springframework.vault.repository.mapping.VaultPersistentEntity; + +/** + * {@link VaultEntityInformation} implementation using a {@link VaultPersistentEntity} + * instance to lookup the necessary information. Can be configured with a custom + * collection to be returned which will trump the one returned by the + * {@link VaultPersistentEntity} if given. + * + * @author Mark Paluch + * @since 2.0 + */ +public class MappingVaultEntityInformation extends + PersistentEntityInformation implements VaultEntityInformation { + + /** + * @param entity + */ + public MappingVaultEntityInformation(VaultPersistentEntity entity) { + + super(entity); + + if (!entity.hasIdProperty()) { + + throw new MappingException( + String.format( + "Entity %s requires to have an explicit id field. Did you forget to provide one using @Id?", + entity.getName())); + } + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultEntityInformation.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultEntityInformation.java new file mode 100644 index 00000000..8ad1cef3 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultEntityInformation.java @@ -0,0 +1,28 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.core; + +import org.springframework.data.repository.core.EntityInformation; + +/** + * Vault-specific {@link EntityInformation}. + * + * @param Domain type. + * @param Id type. + * @since 2.0 + */ +public interface VaultEntityInformation extends EntityInformation { +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java new file mode 100644 index 00000000..c3ffd331 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueAdapter.java @@ -0,0 +1,238 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; + +import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.util.CloseableIterator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.repository.convert.MappingVaultConverter; +import org.springframework.vault.repository.convert.SecretDocument; +import org.springframework.vault.repository.convert.VaultConverter; +import org.springframework.vault.repository.mapping.VaultMappingContext; +import org.springframework.vault.repository.mapping.VaultPersistentEntity; +import org.springframework.vault.repository.mapping.VaultPersistentProperty; +import org.springframework.vault.support.VaultResponse; + +/** + * Vault-specific {@link org.springframework.data.keyvalue.core.KeyValueAdapter}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultKeyValueAdapter extends AbstractKeyValueAdapter { + + private final VaultOperations vaultOperations; + + private final VaultConverter vaultConverter; + + /** + * Create a new {@link VaultKeyValueAdapter} given {@link VaultOperations}. + * + * @param vaultOperations must not be {@literal null}. + */ + public VaultKeyValueAdapter(VaultOperations vaultOperations) { + this(vaultOperations, new MappingVaultConverter(new VaultMappingContext())); + } + + /** + * Create a new {@link VaultKeyValueAdapter} given {@link VaultOperations} and + * {@link VaultConverter}. + * + * @param vaultOperations must not be {@literal null}. + * @param vaultConverter must not be {@literal null}. + */ + public VaultKeyValueAdapter(VaultOperations vaultOperations, + VaultConverter vaultConverter) { + + super(VaultQueryEngine.INSTANCE); + + Assert.notNull(vaultOperations, "VaultOperations must not be null"); + Assert.notNull(vaultConverter, "VaultConverter must not be null"); + + this.vaultOperations = vaultOperations; + this.vaultConverter = vaultConverter; + } + + @Override + public Object put(Object id, Object item, String keyspace) { + + SecretDocument secretDocument = new SecretDocument(id.toString()); + vaultConverter.write(item, secretDocument); + + vaultOperations.write(createKey(id, keyspace), secretDocument.getBody()); + + return secretDocument; + } + + @Override + public boolean contains(Object id, String keyspace) { + return doList(keyspace).contains(id.toString()); + } + + @Nullable + @Override + public Object get(Object id, String keyspace) { + return get(id, keyspace, Object.class); + } + + @Nullable + @Override + public T get(Object id, String keyspace, Class type) { + + VaultResponse response = vaultOperations.read(createKey(id, keyspace)); + + if (response == null) { + return null; + } + + SecretDocument document = SecretDocument.from(id.toString(), response); + + return vaultConverter.read(type, document); + } + + @Nullable + @Override + public Object delete(Object id, String keyspace) { + return delete(id, keyspace, Object.class); + } + + @Nullable + @Override + public T delete(Object id, String keyspace, Class type) { + + T entity = get(id, keyspace, type); + + if (entity == null) { + return null; + } + + vaultOperations.delete(createKey(id, keyspace)); + + return entity; + } + + @Override + public Iterable getAllOf(String keyspace) { + + List list = doList(keyspace); + List items = new ArrayList<>(list.size()); + + for (String id : list) { + items.add(get(id, keyspace)); + } + + return items; + } + + @Override + public CloseableIterator> entries(String keyspace) { + + List list = doList(keyspace); + Iterator iterator = list.iterator(); + + return new CloseableIterator>() { + @Override + public void close() { + + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Entry next() { + + final String key = iterator.next(); + + return new Entry() { + @Override + public Object getKey() { + return key; + } + + @Nullable + @Override + public Object getValue() { + return get(key, keyspace); + } + + @Override + public Object setValue(Object value) { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public void deleteAllOf(String keyspace) { + + List ids = doList(keyspace); + + for (String id : ids) { + vaultOperations.delete(createKey(id, keyspace)); + } + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public long count(String keyspace) { + + List list = doList(keyspace); + + return list.size(); + } + + @Override + public void destroy() throws Exception { + } + + List doList(String keyspace) { + + List list = vaultOperations.list(keyspace); + + return list == null ? Collections.emptyList() : list; + } + + private String createKey(Object id, String keyspace) { + return String.format("%s/%s", keyspace, id); + } + + MappingContext, VaultPersistentProperty> getMappingContext() { + return vaultConverter.getMappingContext(); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java new file mode 100644 index 00000000..02e38e31 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultKeyValueTemplate.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.core; + +import org.springframework.data.keyvalue.core.KeyValueAdapter; +import org.springframework.data.keyvalue.core.KeyValueTemplate; +import org.springframework.vault.repository.mapping.VaultMappingContext; + +/** + * Vault-specific {@link KeyValueTemplate}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultKeyValueTemplate extends KeyValueTemplate { + + /** + * Create a new {@link VaultKeyValueTemplate} given {@link KeyValueAdapter} and + * {@link VaultMappingContext}. + * + * @param adapter must not be {@literal null}. + */ + public VaultKeyValueTemplate(KeyValueAdapter adapter) { + this(adapter, new VaultMappingContext()); + } + + /** + * Create a new {@link VaultKeyValueTemplate} given {@link KeyValueAdapter} and + * {@link VaultMappingContext}. + * + * @param adapter must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public VaultKeyValueTemplate(KeyValueAdapter adapter, + VaultMappingContext mappingContext) { + super(adapter, mappingContext); + } + + @Override + public void destroy() throws Exception { + // no-op to prevent clear() call. + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultQueryEngine.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultQueryEngine.java new file mode 100644 index 00000000..ce044dc2 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/core/VaultQueryEngine.java @@ -0,0 +1,200 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.core; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.domain.Sort.NullHandling; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.keyvalue.core.CriteriaAccessor; +import org.springframework.data.keyvalue.core.QueryEngine; +import org.springframework.data.keyvalue.core.SortAccessor; +import org.springframework.data.keyvalue.core.SpelPropertyComparator; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.context.PersistentPropertyPath; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.vault.repository.query.VaultQuery; + +/** + * Query engine for Vault repository query methods. This engine queries Vault for all + * elements in the keyspace and applies {@link java.util.function.Predicate}s to the + * object id. Queries can contain only predicate subjects pointing to the + * {@link org.springframework.data.annotation.Id} property. + * + * @author Mark Paluch + * @since 2.0 + * @see VaultQuery + * @see org.springframework.vault.repository.query.VaultQueryCreator + */ +class VaultQueryEngine extends + QueryEngine> { + + static final VaultQueryEngine INSTANCE = new VaultQueryEngine(); + + private VaultQueryEngine() { + super(VaultCriteriaAccessor.INSTANCE, SpelSortAccessor.INSTANCE); + } + + @Override + @SuppressWarnings("unchecked") + public Collection execute(VaultQuery vaultQuery, Comparator comparator, + long offset, int rows, String keyspace) { + return execute(vaultQuery, comparator, offset, rows, keyspace, Object.class); + } + + @Override + @SuppressWarnings("unchecked") + public Collection execute(VaultQuery vaultQuery, Comparator comparator, + long offset, int rows, String keyspace, Class type) { + + validatePropertyPaths(vaultQuery); + + Stream stream = getAdapter().doList(keyspace).stream(); + + if (vaultQuery != null) { + stream = stream.filter(vaultQuery::test); + } + + if (comparator == null) { + + if (offset > 0) { + stream = stream.skip(offset); + } + + if (rows > 0) { + stream = stream.limit(rows); + } + } + + Stream typed = stream.map(it -> getAdapter().get(it, keyspace, type)); + + if (comparator != null) { + + typed = typed.sorted((Comparator) comparator); + + if (offset > 0) { + typed = typed.skip(offset); + } + + if (rows > 0) { + typed = typed.limit(rows); + } + } + + return typed.collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public long count(VaultQuery vaultQuery, String keyspace) { + + validatePropertyPaths(vaultQuery); + + Stream stream = getAdapter().doList(keyspace).stream(); + + if (vaultQuery != null) { + stream = stream.filter(vaultQuery::test); + } + + return stream.count(); + } + + private void validatePropertyPaths(VaultQuery vaultQuery) { + + if (vaultQuery == null) { + return; + } + + Stream stream = vaultQuery.getPropertyPaths().stream(); + + stream.map(it -> getAdapter().getMappingContext().getPersistentPropertyPath(it)) + .filter(it -> it.getLeafProperty() != null) + .map(PersistentPropertyPath::getLeafProperty) + .filter(it -> !it.isIdProperty()) + .forEach( + property -> { + throw new InvalidDataAccessApiUsageException(String.format( + "Cannot create criteria for non-@Id property %s", + property)); + }); + } + + enum VaultCriteriaAccessor implements CriteriaAccessor { + + INSTANCE; + @Override + public VaultQuery resolve(KeyValueQuery query) { + return (VaultQuery) query.getCriteria(); + } + } + + /** + * {@link SortAccessor} implementation capable of creating + * {@link SpelPropertyComparator}. + */ + enum SpelSortAccessor implements SortAccessor> { + INSTANCE; + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Comparator resolve(KeyValueQuery query) { + + if (query == null || query.getSort() == null || query.getSort().isUnsorted()) { + return null; + } + + Optional> comparator = Optional.empty(); + for (Order order : query.getSort()) { + + SpelPropertyComparator spelSort = new SpelPropertyComparator<>( + order.getProperty(), parser); + + if (Direction.DESC.equals(order.getDirection())) { + + spelSort.desc(); + + if (!NullHandling.NATIVE.equals(order.getNullHandling())) { + spelSort = NullHandling.NULLS_FIRST.equals(order + .getNullHandling()) ? spelSort.nullsFirst() : spelSort + .nullsLast(); + } + } + + if (!comparator.isPresent()) { + comparator = Optional.of(spelSort); + } + else { + + SpelPropertyComparator spelSortToUse = spelSort; + comparator = comparator.map(it -> it.thenComparing(spelSortToUse)); + } + } + + return comparator + .orElseThrow(() -> new IllegalStateException( + "No sort definitions have been added to this CompoundComparator to compare")); + } + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntity.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntity.java new file mode 100644 index 00000000..86ff2dfd --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntity.java @@ -0,0 +1,71 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.StringUtils; + +/** + * {@link VaultPersistentEntity} implementation. + * + * @author Mark Paluch + * @since 2.0 + */ +public class BasicVaultPersistentEntity extends + BasicKeyValuePersistentEntity implements + VaultPersistentEntity { + + private final String keyspace; + + private final String secretBackend; + + /** + * Creates new {@link BasicVaultPersistentEntity}. + * + * @param information must not be {@literal null}. + * @param fallbackKeySpaceResolver can be {@literal null}. + */ + public BasicVaultPersistentEntity(TypeInformation information, + KeySpaceResolver fallbackKeySpaceResolver) { + super(information, fallbackKeySpaceResolver); + + Secret annotation = findAnnotation(Secret.class); + + String keyspace = super.getKeySpace(); + String secretBackend = "secret"; + + if (annotation != null) { + if (StringUtils.hasText(annotation.backend())) { + secretBackend = annotation.backend(); + } + } + + this.secretBackend = secretBackend; + this.keyspace = String.format("%s/%s", secretBackend, keyspace); + } + + @Override + public String getKeySpace() { + return keyspace; + } + + @Override + public String getSecretBackend() { + return secretBackend; + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/Secret.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/Secret.java new file mode 100644 index 00000000..2b36c7da --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/Secret.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +/** + * @author Mark Paluch + */ + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.annotation.Persistent; +import org.springframework.data.keyvalue.annotation.KeySpace; + +/** + * {@link Secret} marks objects as aggregate roots to be stored in Vault. + * + * @author Mark Paluch + */ +@Persistent +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(value = { ElementType.TYPE }) +@KeySpace +public @interface Secret { + + /** + * The prefix to distinguish between domain types. + * @see KeySpace + */ + @AliasFor(annotation = KeySpace.class, attribute = "value") + String value() default ""; + + /** + * Secret backend mount, defaults to {@literal secret}. + */ + String backend() default "secret"; +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultMappingContext.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultMappingContext.java new file mode 100644 index 00000000..2473cb9c --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultMappingContext.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver; +import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Mapping context for {@link VaultPersistentEntity Vault-specific entities}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultMappingContext extends + KeyValueMappingContext, VaultPersistentProperty> { + + private KeySpaceResolver fallbackKeySpaceResolver = SimpleClassNameKeySpaceResolver.INSTANCE; + + public KeySpaceResolver getFallbackKeySpaceResolver() { + return fallbackKeySpaceResolver; + } + + @Override + public void setFallbackKeySpaceResolver(KeySpaceResolver fallbackKeySpaceResolver) { + this.fallbackKeySpaceResolver = fallbackKeySpaceResolver; + } + + @Override + protected VaultPersistentEntity createPersistentEntity( + TypeInformation typeInformation) { + return new BasicVaultPersistentEntity<>(typeInformation, fallbackKeySpaceResolver); + } + + @Override + protected VaultPersistentProperty createPersistentProperty(Property property, + VaultPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new VaultPersistentProperty(property, owner, simpleTypeHolder); + } + + /** + * Most trivial implementation of {@link KeySpaceResolver} returning the + * {@link Class#getName()}. + * + * @author Mark Paluch + */ + enum SimpleClassNameKeySpaceResolver implements KeySpaceResolver { + + INSTANCE; + + @Override + public String resolveKeySpace(Class type) { + + Assert.notNull(type, "Type must not be null!"); + return StringUtils + .uncapitalize(ClassUtils.getUserClass(type).getSimpleName()); + } + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentEntity.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentEntity.java new file mode 100644 index 00000000..6a50e33a --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentEntity.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.mapping.PersistentEntity; + +/** + * Vault specific {@link PersistentEntity}. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface VaultPersistentEntity extends + KeyValuePersistentEntity { + + /** + * @return the secret backend in which this {@link PersistentEntity} is stored. + */ + String getSecretBackend(); +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentProperty.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentProperty.java new file mode 100644 index 00000000..573b8ff0 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultPersistentProperty.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.model.Property; +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Vault-specific {@link KeyValuePersistentProperty}. By default, if a property is named + * {@code id} it's used as Id property. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultPersistentProperty extends + KeyValuePersistentProperty { + + private static final Set SUPPORTED_ID_PROPERTY_NAMES = new HashSet(); + + static { + SUPPORTED_ID_PROPERTY_NAMES.add("id"); + } + + /** + * Create a new {@link VaultPersistentProperty}. + * + * @param property must not be {@literal null}. + * @param owner must not be {@literal null}. + * @param simpleTypeHolder must not be {@literal null}. + */ + public VaultPersistentProperty(Property property, + PersistentEntity owner, + SimpleTypeHolder simpleTypeHolder) { + + super(property, owner, simpleTypeHolder); + } + + @Override + public boolean isIdProperty() { + return super.isIdProperty() || SUPPORTED_ID_PROPERTY_NAMES.contains(getName()); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultSimpleTypes.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultSimpleTypes.java new file mode 100644 index 00000000..ab0372d2 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/mapping/VaultSimpleTypes.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.vault.repository.convert.SecretDocument; + +/** + * Simple constant holder for a {@link SimpleTypeHolder} enriched with Vault-specific + * simple (JSON) types. + * + * @author Mark Paluch + * @since 2.0 + */ +public abstract class VaultSimpleTypes { + + static { + + Set> simpleTypes = new HashSet>(); + simpleTypes.add(SecretDocument.class); + + VAULT_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes); + } + + private static final Set> VAULT_SIMPLE_TYPES; + public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder( + VAULT_SIMPLE_TYPES, true); + + private VaultSimpleTypes() { + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQuery.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQuery.java new file mode 100644 index 00000000..ff9b66ca --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQuery.java @@ -0,0 +1,175 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.query; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +import org.springframework.data.mapping.PropertyPath; +import org.springframework.util.Assert; +import org.springframework.vault.repository.convert.SecretDocument; + +/** + * Vault query consisting of a single {@link Predicate}. A new (empty) query evaluates + * unconditionally to {@literal true} and can be composed using {@link #and(VaultQuery)} + * and {@link #or(VaultQuery)}. + *

+ * A query can express predicates only against the + * {@link org.springframework.data.annotation.Id} field of a {@link SecretDocument}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultQuery { + + private final Predicate predicate; + + private final Set propertyPaths; + + /** + * Create a new {@link VaultQuery} that evaluates unconditionally to {@literal true}. + */ + public VaultQuery() { + this(s -> true); + } + + /** + * Create a new {@link VaultQuery} given {@link Predicate}. + * + * @param predicate must not be {@literal null}. + */ + public VaultQuery(Predicate predicate) { + this(predicate, Collections.emptySet()); + } + + /** + * Create a new {@link VaultQuery} given {@link Predicate} and {@link PropertyPath}. + * + * @param predicate must not be {@literal null}. + */ + public VaultQuery(Predicate predicate, PropertyPath propertyPath) { + + Assert.notNull(predicate, "Predicate must not be null"); + Assert.notNull(propertyPath, "PropertyPath must not be null"); + + this.predicate = predicate; + this.propertyPaths = Collections.singleton(propertyPath); + } + + private VaultQuery(Predicate predicate, Set propertyPaths) { + + Assert.notNull(propertyPaths, "PropertyPaths must not be null"); + Assert.notNull(predicate, "Predicate must not be null"); + + this.predicate = predicate; + this.propertyPaths = propertyPaths; + } + + /** + * Evaluate the query against a {@link SecretDocument}. + * + * @param document must not be {@literal null}. + * @return {@literal true} if the predicate matches, {@literal false} otherwise. + */ + public boolean test(SecretDocument document) { + + Assert.notNull(predicate, "Predicate must not be null"); + + return predicate.test(document.getId()); + } + + /** + * Evaluate the query against a {@link String}. + * + * @param id must not be {@literal null}. + * @return {@literal true} if the predicate matches, {@literal false} otherwise. + */ + public boolean test(String id) { + + Assert.notNull(id, "Id to test must not be null"); + + return predicate.test(id); + } + + /** + * Compose a new {@link VaultQuery} using predicates of {@literal this} and the + * {@code other} query using logical {@code AND}. + * + * @param other must not be {@literal null}. + * @return a new composed {@link VaultQuery}. + * @see Predicate#and(Predicate) + */ + public VaultQuery and(VaultQuery other) { + + Set propertyPaths = new HashSet<>(this.propertyPaths.size() + + other.propertyPaths.size(), 1); + propertyPaths.addAll(this.propertyPaths); + propertyPaths.addAll(other.propertyPaths); + + return new VaultQuery(this.predicate.and(other.predicate), propertyPaths); + } + + /** + * Compose a new {@link VaultQuery} using predicates of {@literal this} and the + * {@code other} query using logical {@code OR}. + * + * @param other must not be {@literal null}. + * @return a new composed {@link VaultQuery}. + * @see Predicate#and(Predicate) + */ + public VaultQuery or(VaultQuery other) { + + Set propertyPaths = new HashSet<>(this.propertyPaths.size() + + other.propertyPaths.size(), 1); + propertyPaths.addAll(this.propertyPaths); + propertyPaths.addAll(other.propertyPaths); + + return new VaultQuery(this.predicate.or(other.predicate), propertyPaths); + } + + /** + * Compose a new {@link VaultQuery} using predicates of {@literal this} query and the + * {@code other} {@link Predicate} using logical {@code AND}. + * + * @param other must not be {@literal null}. + * @return a new composed {@link VaultQuery}. + * @see Predicate#and(Predicate) + */ + public VaultQuery and(Predicate predicate, PropertyPath propertyPath) { + + Set propertyPaths = new HashSet<>(this.propertyPaths.size() + 1, 1); + propertyPaths.addAll(this.propertyPaths); + propertyPaths.add(propertyPath); + + return new VaultQuery(this.predicate.and(predicate), propertyPaths); + } + + /** + * @return the underlying predicate. + */ + public Predicate getPredicate() { + return predicate; + } + + /** + * @return constrained {@link PropertyPath}s for this {@link VaultQuery}. + */ + public Set getPropertyPaths() { + return Collections.unmodifiableSet(propertyPaths); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQueryCreator.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQueryCreator.java new file mode 100644 index 00000000..0a039b1d --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/query/VaultQueryCreator.java @@ -0,0 +1,259 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.query; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import lombok.Value; + +import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.repository.query.parser.Part.IgnoreCaseType; +import org.springframework.data.repository.query.parser.Part.Type; + +/** + * Query creator for Vault queries. Vault queries are limited to criterias constraining + * the {@link org.springframework.data.annotation.Id} property. A query consists of + * chained {@link Predicate}s that are evaluated for each Id value. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultQueryCreator extends + AbstractQueryCreator, VaultQuery> { + + /** + * Create a new {@link VaultQueryCreator} given {@link PartTree}. + * + * @param tree must not be {@literal null}. + */ + public VaultQueryCreator(PartTree tree) { + super(tree); + } + + /** + * Create a new {@link VaultQueryCreator} given {@link PartTree} and + * {@link ParameterAccessor}. + * + * @param tree must not be {@literal null}. + * @param parameters must not be {@literal null}. + */ + public VaultQueryCreator(PartTree tree, ParameterAccessor parameters) { + super(tree, parameters); + } + + @Override + protected VaultQuery create(Part part, Iterator parameters) { + return new VaultQuery(createPredicate(part, parameters), part.getProperty()); + } + + @Override + protected VaultQuery and(Part part, VaultQuery base, Iterator parameters) { + + if (base == null) { + return create(part, parameters); + } + return base.and(createPredicate(part, parameters), part.getProperty()); + } + + private static Predicate createPredicate(Part part, + Iterator parameters) { + + VariableAccessor accessor = getVariableAccessor(part); + + Predicate predicate = from(part, accessor, parameters); + + return it -> predicate.test(accessor.toString(it)); + } + + /** + * Return a {@link Predicate} depending on the {@link Part} given. + * + * @param part + * @param parameters + * @return + */ + private static Predicate from(Part part, VariableAccessor accessor, + Iterator parameters) { + + Type type = part.getType(); + + switch (type) { + case AFTER: + case GREATER_THAN: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.compareTo(value) > 0); + case GREATER_THAN_EQUAL: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.compareTo(value) >= 0); + case BEFORE: + case LESS_THAN: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.compareTo(value) < 0); + case LESS_THAN_EQUAL: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.compareTo(value) <= 0); + case BETWEEN: + + String from = accessor.nextString(parameters); + String to = accessor.nextString(parameters); + + return it -> it.compareTo(from) >= 0 && it.compareTo(to) <= 0; + case NOT_IN: + return new Criteria<>(accessor.nextAsArray(parameters), + (value, it) -> Arrays.binarySearch(value, it) < 0); + case IN: + return new Criteria<>(accessor.nextAsArray(parameters), + (value, it) -> Arrays.binarySearch(value, it) >= 0); + case STARTING_WITH: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.startsWith(value)); + case ENDING_WITH: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.endsWith(value)); + case CONTAINING: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.contains(value)); + case NOT_CONTAINING: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> !it.contains(value)); + case REGEX: + return Pattern.compile((String) parameters.next(), + isIgnoreCase(part) ? Pattern.CASE_INSENSITIVE : 0).asPredicate(); + case TRUE: + return it -> it.equalsIgnoreCase("true"); + case FALSE: + return it -> it.equalsIgnoreCase("false"); + case SIMPLE_PROPERTY: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> it.equals(value)); + case NEGATING_SIMPLE_PROPERTY: + return new Criteria<>(accessor.nextString(parameters), + (value, it) -> !it.equals(value)); + default: + throw new IllegalArgumentException("Unsupported keyword!"); + } + } + + @Override + protected VaultQuery or(VaultQuery vaultQuery, VaultQuery other) { + return vaultQuery.or(other); + } + + @Override + protected KeyValueQuery complete(VaultQuery vaultQuery, Sort sort) { + + KeyValueQuery query = new KeyValueQuery<>(vaultQuery); + + if (sort != null) { + query.orderBy(sort); + } + + return query; + } + + private static VariableAccessor getVariableAccessor(Part part) { + return isIgnoreCase(part) ? VariableAccessor.Lowercase : VariableAccessor.AsIs; + } + + private static boolean isIgnoreCase(Part part) { + return part.shouldIgnoreCase() != IgnoreCaseType.NEVER; + } + + @Value + static class Criteria implements Predicate { + + private T value; + private BiPredicate predicate; + + @Override + public boolean test(String s) { + return predicate.test(value, s); + } + } + + enum VariableAccessor { + + AsIs { + + @Override + String nextString(Iterator parameters) { + return parameters.next().toString(); + } + + @Override + String[] nextAsArray(Iterator iterator) { + + Object next = iterator.next(); + + if (next instanceof Collection) { + return ((Collection) next).toArray(new String[0]); + } + else if (next != null && next.getClass().isArray()) { + return (String[]) next; + } + + return new String[] { (String) next }; + } + + @Override + String toString(String value) { + return value; + } + }, + + Lowercase { + + @Override + String nextString(Iterator parameters) { + return AsIs.nextString(parameters).toLowerCase(); + } + + @Override + String[] nextAsArray(Iterator iterator) { + + String[] original = AsIs.nextAsArray(iterator); + String[] lowercase = new String[original.length]; + + for (int i = 0; i < original.length; i++) { + lowercase[i] = original[i].toLowerCase(); + } + + return lowercase; + } + + @Override + String toString(String value) { + return value.toLowerCase(); + } + }; + + abstract String[] nextAsArray(Iterator iterator); + + abstract String nextString(Iterator iterator); + + abstract String toString(String value); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java new file mode 100644 index 00000000..359fcc86 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.support; + +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory; +import org.springframework.data.repository.core.EntityInformation; +import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.vault.repository.core.MappingVaultEntityInformation; +import org.springframework.vault.repository.mapping.VaultPersistentEntity; +import org.springframework.vault.repository.query.VaultQueryCreator; + +/** + * {@link RepositoryFactorySupport} specific of handing Vault + * {@link org.springframework.data.keyvalue.repository.KeyValueRepository}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class VaultRepositoryFactory extends KeyValueRepositoryFactory { + + private final KeyValueOperations operations; + + public VaultRepositoryFactory(KeyValueOperations keyValueOperations) { + this(keyValueOperations, VaultQueryCreator.class); + } + + public VaultRepositoryFactory(KeyValueOperations keyValueOperations, + Class> queryCreator) { + this(keyValueOperations, queryCreator, KeyValuePartTreeQuery.class); + } + + public VaultRepositoryFactory(KeyValueOperations keyValueOperations, + Class> queryCreator, + Class repositoryQueryType) { + super(keyValueOperations, queryCreator, repositoryQueryType); + + this.operations = keyValueOperations; + } + + @Override + @SuppressWarnings("unchecked") + public EntityInformation getEntityInformation(Class domainClass) { + + VaultPersistentEntity entity = (VaultPersistentEntity) operations + .getMappingContext().getPersistentEntity(domainClass); + + return new MappingVaultEntityInformation<>(entity); + } +} diff --git a/spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactoryBean.java b/spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactoryBean.java new file mode 100644 index 00000000..16a4cf36 --- /dev/null +++ b/spring-vault-repository/src/main/java/org/springframework/vault/repository/support/VaultRepositoryFactoryBean.java @@ -0,0 +1,57 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.support; + +import java.io.Serializable; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; + +/** + * Adapter for Springs {@link FactoryBean} interface to allow easy setup of + * {@link VaultRepositoryFactory} via Spring configuration. + * + * @param The repository type. + * @param The repository domain type. + * @param The repository id type. + * @since 2.0 + */ +public class VaultRepositoryFactoryBean, S, ID extends Serializable> + extends KeyValueRepositoryFactoryBean { + + /** + * Creates a new {@link VaultRepositoryFactoryBean} for the given repository + * interface. + * + * @param repositoryInterface must not be {@literal null}. + */ + public VaultRepositoryFactoryBean(Class repositoryInterface) { + super(repositoryInterface); + } + + @Override + protected VaultRepositoryFactory createRepositoryFactory( + KeyValueOperations operations, + Class> queryCreator, + Class repositoryQueryType) { + + return new VaultRepositoryFactory(operations, queryCreator, repositoryQueryType); + } +} diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultIntegrationTestConfiguration.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultIntegrationTestConfiguration.java new file mode 100644 index 00000000..981f2eed --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultIntegrationTestConfiguration.java @@ -0,0 +1,48 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository; + +import org.springframework.context.annotation.Configuration; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.config.AbstractVaultConfiguration; +import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.util.Settings; + +/** + * Test configuration for Vault integration tests. + * + * @author Mark Paluch + */ +@Configuration +public class VaultIntegrationTestConfiguration extends AbstractVaultConfiguration { + + @Override + public VaultEndpoint vaultEndpoint() { + return new VaultEndpoint(); + } + + @Override + public ClientAuthentication clientAuthentication() { + return new TokenAuthentication(Settings.token()); + } + + @Override + public SslConfiguration sslConfiguration() { + return Settings.createSslConfiguration(); + } +} diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultRepositoryIntegrationTests.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultRepositoryIntegrationTests.java new file mode 100644 index 00000000..6b873662 --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/VaultRepositoryIntegrationTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository; + +import java.util.List; + +import lombok.Data; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Sort; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.vault.core.VaultTemplate; +import org.springframework.vault.repository.VaultRepositoryIntegrationTests.VaultRepositoryTestConfiguration; +import org.springframework.vault.repository.configuration.EnableVaultRepositories; +import org.springframework.vault.util.IntegrationTestSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.domain.Sort.Order.asc; + +/** + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = VaultRepositoryTestConfiguration.class) +public class VaultRepositoryIntegrationTests extends IntegrationTestSupport { + + @Configuration + @EnableVaultRepositories(considerNestedRepositories = true) + static class VaultRepositoryTestConfiguration extends + VaultIntegrationTestConfiguration { + } + + @Autowired + VaultRepository vaultRepository; + + @Autowired + VaultTemplate vaultTemplate; + + @Before + public void before() { + vaultRepository.deleteAll(); + } + + @Test + public void loadAndSave() { + + Person person = new Person(); + person.setId("foo-key"); + person.setName("bar"); + + vaultRepository.save(person); + + Iterable all = vaultRepository.findAll(); + + assertThat(all).contains(person); + assertThat(vaultRepository.findById("foo-key")).contains(person); + } + + @Test + public void shouldApplyQueryMethod() { + + Person walter = new Person(); + walter.setId("walter"); + walter.setName("Walter"); + + vaultRepository.save(walter); + + Person skyler = new Person(); + skyler.setId("skyler"); + skyler.setName("Skyler"); + + vaultRepository.save(skyler); + + Iterable all = vaultRepository.findByIdStartsWith("walt"); + + assertThat(all).contains(walter).doesNotContain(skyler); + } + + @Test + public void shouldApplyQueryMethodWithSorting() { + + Person walter = new Person(); + walter.setId("walter"); + walter.setName("Walter"); + + vaultRepository.save(walter); + + Person skyler = new Person(); + skyler.setId("skyler"); + skyler.setName("Skyler"); + + vaultRepository.save(skyler); + + assertThat(vaultRepository.findAllByOrderByNameAsc()).containsSequence(skyler, + walter); + assertThat(vaultRepository.findAllByOrderByNameDesc()).containsSequence(walter, + skyler); + } + + @Test + public void shouldApplyLimiting() { + + Person walter = new Person(); + walter.setId("walter"); + walter.setName("Walter"); + + vaultRepository.save(walter); + + Person skyler = new Person(); + skyler.setId("skyler"); + skyler.setName("Skyler"); + + vaultRepository.save(skyler); + + assertThat(vaultRepository.findTop1By(Sort.by(asc("name")))).containsOnly(skyler); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void shouldFailForNonIdCriteria() { + vaultRepository.findInvalidByName("foo"); + } + + interface VaultRepository extends CrudRepository { + + List findByIdStartsWith(String prefix); + + List findAllByOrderByNameAsc(); + + List findAllByOrderByNameDesc(); + + List findTop1By(Sort sort); + + List findInvalidByName(String name); + } + + @Data + static class Person { + + String id, name; + } +} diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapperUnitTests.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapperUnitTests.java new file mode 100644 index 00000000..3a866339 --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/DefaultVaultTypeMapperUnitTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.data.convert.ConfigurableTypeInformationMapper; +import org.springframework.data.convert.SimpleTypeInformationMapper; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DefaultVaultTypeMapper}. + * + * @author Mark Paluch + */ +public class DefaultVaultTypeMapperUnitTests { + + ConfigurableTypeInformationMapper configurableTypeInformationMapper = new ConfigurableTypeInformationMapper( + Collections.singletonMap(String.class, "1")); + SimpleTypeInformationMapper simpleTypeInformationMapper = new SimpleTypeInformationMapper(); + DefaultVaultTypeMapper typeMapper = new DefaultVaultTypeMapper(); + + @Test + public void defaultInstanceWritesClasses() { + + writesTypeToField(new LinkedHashMap<>(), String.class, String.class.getName()); + } + + @Test + public void defaultInstanceReadsClasses() { + + Map document = new LinkedHashMap<>(); + document.put(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, String.class.getName()); + + readsTypeFromField(document, String.class); + } + + @Test + public void writesMapKeyForType() { + + typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, + Collections.singletonList(configurableTypeInformationMapper)); + + writesTypeToField(new LinkedHashMap<>(), String.class, "1"); + writesTypeToField(new LinkedHashMap<>(), Object.class, null); + } + + @Test + public void writesClassNamesForUnmappedValuesIfConfigured() { + + typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, + Arrays.asList(configurableTypeInformationMapper, + simpleTypeInformationMapper)); + writesTypeToField(new LinkedHashMap<>(), String.class, "1"); + writesTypeToField(new LinkedHashMap<>(), Object.class, Object.class.getName()); + } + + @Test + public void readsTypeForMapKey() { + + typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, + Collections.singletonList(configurableTypeInformationMapper)); + + readsTypeFromField( + Collections.singletonMap(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "1"), + String.class); + readsTypeFromField(Collections.singletonMap( + DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "unmapped"), null); + } + + @Test + public void readsTypeLoadingClassesForUnmappedTypesIfConfigured() { + + typeMapper = new DefaultVaultTypeMapper(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, + Arrays.asList(configurableTypeInformationMapper, + simpleTypeInformationMapper)); + + readsTypeFromField( + Collections.singletonMap(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "1"), + String.class); + readsTypeFromField(Collections.singletonMap( + DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, Object.class.getName()), + Object.class); + } + + @Test + public void addsFullyQualifiedClassNameUnderDefaultKeyByDefault() { + writesTypeToField(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, new LinkedHashMap<>(), + String.class); + } + + @Test + public void writesTypeToCustomFieldIfConfigured() { + + typeMapper = new DefaultVaultTypeMapper("_custom"); + writesTypeToField("_custom", new LinkedHashMap<>(), String.class); + } + + @Test + public void doesNotWriteTypeInformationInCaseKeyIsSetToNull() { + + typeMapper = new DefaultVaultTypeMapper(null); + writesTypeToField(null, new LinkedHashMap<>(), String.class); + } + + @Test + public void readsTypeFromDefaultKeyByDefault() { + readsTypeFromField(Collections.singletonMap( + DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, String.class.getName()), + String.class); + } + + @Test + public void readsTypeFromCustomFieldConfigured() { + + typeMapper = new DefaultVaultTypeMapper("_custom"); + readsTypeFromField(Collections.singletonMap("_custom", String.class.getName()), + String.class); + } + + @Test + public void returnsNullIfNoTypeInfoInDocument() { + readsTypeFromField(new LinkedHashMap<>(), null); + readsTypeFromField( + Collections.singletonMap(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, ""), + null); + } + + @Test + public void returnsNullIfClassCannotBeLoaded() { + readsTypeFromField(Collections.singletonMap( + DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, "fooBar"), null); + } + + @Test + public void returnsNullIfTypeKeySetToNull() { + typeMapper = new DefaultVaultTypeMapper(null); + readsTypeFromField(Collections.singletonMap( + DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, String.class), null); + } + + @Test + public void returnsCorrectTypeKey() { + + assertThat(typeMapper.isTypeKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY)) + .isTrue(); + + typeMapper = new DefaultVaultTypeMapper("_custom"); + assertThat(typeMapper.isTypeKey("_custom")).isTrue(); + assertThat(typeMapper.isTypeKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY)) + .isFalse(); + + typeMapper = new DefaultVaultTypeMapper(null); + assertThat(typeMapper.isTypeKey("_custom")).isFalse(); + assertThat(typeMapper.isTypeKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY)) + .isFalse(); + } + + private void readsTypeFromField(Map document, @Nullable Class type) { + + TypeInformation typeInfo = typeMapper.readType(document); + + if (type != null) { + assertThat(typeInfo).isNotNull(); + assertThat(typeInfo.getType()).isAssignableFrom(type); + } + else { + assertThat(typeInfo).isNull(); + } + } + + private void writesTypeToField(@Nullable String field, Map document, + Class type) { + + typeMapper.writeType(type, document); + + if (field == null) { + assertThat(document.keySet()).isEmpty(); + } + else { + assertThat(document).containsKey(field); + assertThat(document).containsEntry(field, type.getName()); + } + } + + private void writesTypeToField(Map document, Class type, + @Nullable Object value) { + + typeMapper.writeType(type, document); + + if (value == null) { + assertThat(document.keySet()).isEmpty(); + } + else { + assertThat(document).containsKey(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY); + assertThat(document).containsEntry(DefaultVaultTypeMapper.DEFAULT_TYPE_KEY, + value); + } + } +} diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/MappingVaultConverterUnitTests.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/MappingVaultConverterUnitTests.java new file mode 100644 index 00000000..4dbcc0d4 --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/convert/MappingVaultConverterUnitTests.java @@ -0,0 +1,380 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.convert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.vault.repository.mapping.VaultMappingContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MappingVaultConverter}. + * + * @author Mark Paluch + */ +public class MappingVaultConverterUnitTests { + + VaultMappingContext context = new VaultMappingContext(); + + MappingVaultConverter converter = new MappingVaultConverter(context); + + @Before + public void before() { + + VaultCustomConversions conversions = new VaultCustomConversions(Arrays.asList( + DocumentToPersonConverter.INSTANCE, PersonToDocumentConverter.INSTANCE)); + converter.setCustomConversions(conversions); + converter.afterPropertiesSet(); + } + + @Test + public void shouldReadSimpleEntity() { + + SecretDocument document = new SecretDocument("heisenberg"); + document.put("username", "walter"); + document.put("password", "hb"); + + SimpleEntity entity = converter.read(SimpleEntity.class, document); + + assertThat(entity.getId()).isEqualTo("heisenberg"); + assertThat(entity.getUsername()).isEqualTo("walter"); + assertThat(entity.getPassword()).isEqualTo("hb"); + } + + @Test + public void shouldReadConvertedEntity() { + + SecretDocument document = new SecretDocument("heisenberg"); + document.put("the_name", "walter"); + + Person entity = converter.read(Person.class, document); + + assertThat(entity.getName()).isEqualTo("walter"); + } + + @Test + public void shouldReadSubtype() { + + SecretDocument document = new SecretDocument("heisenberg"); + document.put("_class", ExtendedEntity.class.getName()); + document.put("username", "walter"); + document.put("password", "hb"); + document.put("location", "Albuquerque"); + + SimpleEntity entity = converter.read(SimpleEntity.class, document); + + assertThat(entity).isInstanceOf(ExtendedEntity.class); + assertThat(entity.getId()).isEqualTo("heisenberg"); + assertThat(entity.getUsername()).isEqualTo("walter"); + assertThat(entity.getPassword()).isEqualTo("hb"); + assertThat(((ExtendedEntity) entity).getLocation()).isEqualTo("Albuquerque"); + } + + @Test + public void shouldReadEntityWithEnum() { + + SecretDocument document = new SecretDocument(); + document.put("condition", "BAD"); + + EntityWithEnum entity = converter.read(EntityWithEnum.class, document); + + assertThat(entity.getCondition()).isEqualTo(Condition.BAD); + } + + @Test + public void shouldReadSimpleEntityWithConstructorCreation() { + + SecretDocument document = new SecretDocument("heisenberg"); + document.put("username", "walter"); + document.put("password", "hb"); + + ConstructorCreation entity = converter.read(ConstructorCreation.class, document); + + assertThat(entity.getId()).isEqualTo("heisenberg"); + assertThat(entity.getUsername()).isEqualTo("walter"); + assertThat(entity.getPassword()).isEqualTo("hb"); + } + + @Test + public void shouldReadEntityWithList() { + + SecretDocument document = new SecretDocument(Collections.singletonMap( + "usernames", Arrays.asList("walter", "heisenberg"))); + + EntityWithListOfStrings entity = converter.read(EntityWithListOfStrings.class, + document); + + assertThat(entity.getUsernames()).containsSequence("walter", "heisenberg"); + } + + @Test + public void shouldReadEntityWithMap() { + + Map keyVersions = new LinkedHashMap<>(); + keyVersions.put("foo", 1); + keyVersions.put("bar", 2); + + SecretDocument document = new SecretDocument(Collections.singletonMap( + "keyVersions", keyVersions)); + + EntityWithMap entity = converter.read(EntityWithMap.class, document); + + assertThat(entity.getKeyVersions()).containsAllEntriesOf(keyVersions); + } + + @Test + public void shouldReadEntityWithNesting() { + + Map walter = new LinkedHashMap<>(); + walter.put("username", "heisenberg"); + walter.put("password", "hb"); + + SecretDocument document = new SecretDocument(); + document.put("nested", walter); + + EntityWithNestedType entity = converter + .read(EntityWithNestedType.class, document); + + assertThat(entity.getNested()).isEqualTo(new NestedType("heisenberg", "hb")); + } + + @Test + public void shouldReadEntityWithListOfEntities() { + + Map walter = new LinkedHashMap<>(); + walter.put("username", "heisenberg"); + walter.put("password", "hb"); + + Map skyler = new LinkedHashMap<>(); + skyler.put("username", "skyler"); + skyler.put("password", "marie"); + + SecretDocument document = new SecretDocument(); + document.put("nested", Arrays.asList(walter, skyler)); + + EntityWithListOfEntities entity = converter.read(EntityWithListOfEntities.class, + document); + + assertThat(entity.getNested()).contains(new NestedType("heisenberg", "hb"), + new NestedType("skyler", "marie")); + } + + @Test + public void shouldWriteSimpleEntity() { + + SimpleEntity entity = new SimpleEntity(); + entity.setId("heisenberg"); + entity.setUsername("walter"); + entity.setPassword("hb"); + + SecretDocument expected = new SecretDocument("heisenberg"); + expected.put("username", "walter"); + expected.put("password", "hb"); + expected.put("_class", entity.getClass().getName()); + + SecretDocument sink = new SecretDocument(); + + converter.write(entity, sink); + + assertThat(sink).isEqualTo(expected); + } + + @Test + public void shouldWriteConvertedEntity() { + + SecretDocument expected = new SecretDocument(); + expected.put("the_name", "walter"); + + SecretDocument sink = new SecretDocument(); + + converter.write(new Person("walter"), sink); + + assertThat(sink).isEqualTo(expected); + } + + @Test + public void shouldWriteEntityWithEnum() { + + EntityWithEnum entity = new EntityWithEnum(); + entity.setCondition(Condition.BAD); + + SecretDocument sink = new SecretDocument(); + + converter.write(entity, sink); + + assertThat(sink.getBody()).containsEntry("condition", "BAD"); + } + + @Test + public void shouldWriteEntityWithList() { + + EntityWithListOfStrings entity = new EntityWithListOfStrings(); + entity.setUsernames(Arrays.asList("walter", "heisenberg")); + + SecretDocument sink = new SecretDocument(); + + converter.write(entity, sink); + + assertThat(sink.getBody()).containsEntry("usernames", + Arrays.asList("walter", "heisenberg")); + } + + @Test + public void shouldWriteEntityWithMap() { + + Map keyVersions = new LinkedHashMap<>(); + keyVersions.put("foo", 1); + keyVersions.put("bar", 2); + + EntityWithMap entity = new EntityWithMap(); + entity.setKeyVersions(keyVersions); + + SecretDocument sink = new SecretDocument(); + + converter.write(entity, sink); + + assertThat(sink.getBody()).containsEntry("keyVersions", keyVersions); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldWriteEntityWithListOfEntities() { + + EntityWithListOfEntities entity = new EntityWithListOfEntities(); + entity.setNested(Arrays.asList(new NestedType("heisenberg", "hb"), + new NestedType("skyler", "marie"))); + + Map walter = new LinkedHashMap<>(); + walter.put("username", "heisenberg"); + walter.put("password", "hb"); + + Map skyler = new LinkedHashMap<>(); + skyler.put("username", "skyler"); + skyler.put("password", "marie"); + + SecretDocument sink = new SecretDocument(); + + converter.write(entity, sink); + + assertThat((List>) sink.get("nested")).contains(walter, + skyler); + } + + @Data + static class SimpleEntity { + + String id; + String username; + String password; + } + + @Data + static class ExtendedEntity extends SimpleEntity { + + String location; + } + + @Data + static class EntityWithNestedType { + + NestedType nested; + } + + @Data + static class EntityWithEnum { + + Condition condition; + } + + @Data + @RequiredArgsConstructor + static class ConstructorCreation { + + final String id; + final String username; + String password; + } + + @Data + static class EntityWithListOfStrings { + + List usernames; + } + + @Data + static class EntityWithListOfEntities { + + List nested; + } + + @Data + static class EntityWithMap { + + Map keyVersions; + } + + @Data + @AllArgsConstructor + static class NestedType { + + String username; + String password; + } + + enum Condition { + GOOD, BAD + } + + @Data + static class Person { + final String name; + } + + enum DocumentToPersonConverter implements Converter { + + INSTANCE; + + @Override + public Person convert(SecretDocument secretDocument) { + return new Person((String) secretDocument.get("the_name")); + } + } + + enum PersonToDocumentConverter implements Converter { + + INSTANCE; + + @Override + public SecretDocument convert(Person person) { + + SecretDocument document = new SecretDocument(); + document.put("the_name", person.getName()); + return document; + } + } +} diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntityUnitTests.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntityUnitTests.java new file mode 100644 index 00000000..a4e3313b --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/BasicVaultPersistentEntityUnitTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import org.junit.Test; + +import org.springframework.data.annotation.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link BasicVaultPersistentEntity} via {@link VaultMappingContext}. + * + * @author Mark Paluch + */ +public class BasicVaultPersistentEntityUnitTests { + + private VaultMappingContext mappingContext = new VaultMappingContext(); + + @Test + public void shouldSetIdPropertyThroughName() { + + VaultPersistentEntity persistentEntity = mappingContext + .getPersistentEntity(IdProperty.class); + + assertThat(persistentEntity.getIdProperty()).isNotNull(); + } + + @Test + public void shouldSetIdPropertyThroughAnnotation() { + + VaultPersistentEntity persistentEntity = mappingContext + .getPersistentEntity(ExplicitId.class); + + assertThat(persistentEntity.getIdProperty()).isNotNull(); + } + + static class IdProperty { + String id, username; + } + + static class ExplicitId { + @Id + String username; + } + +} \ No newline at end of file diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/VaultMappingContextUnitTests.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/VaultMappingContextUnitTests.java new file mode 100644 index 00000000..b74a4957 --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/mapping/VaultMappingContextUnitTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.mapping; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link VaultMappingContext}. + * + * @author Mark Paluch + */ +public class VaultMappingContextUnitTests { + + VaultMappingContext context = new VaultMappingContext(); + + @Test + public void shouldCreatePersistentEntity() { + + VaultPersistentEntity entity = context.getPersistentEntity(Person.class); + + assertThat(entity).isNotNull(); + assertThat(entity.getSecretBackend()).isEqualTo("secret"); + assertThat(entity.getKeySpace()).isEqualTo("secret/person"); + } + + @Test + public void shouldDetermineKeyspace() { + + assertThat(context.getRequiredPersistentEntity(Login.class).getSecretBackend()) + .isEqualTo("secret"); + assertThat(context.getRequiredPersistentEntity(Login.class).getKeySpace()) + .isEqualTo("secret/login"); + + assertThat( + context.getRequiredPersistentEntity(Credentials.class).getSecretBackend()) + .isEqualTo("shared"); + assertThat(context.getRequiredPersistentEntity(Credentials.class).getKeySpace()) + .isEqualTo("shared/Email"); + } + + private static class Person { + } + + @Secret + private static class Login { + } + + @Secret(value = "Email", backend = "shared") + private static class Credentials { + } +} diff --git a/spring-vault-repository/src/test/java/org/springframework/vault/repository/query/VaultQueryCreatorUnitTests.java b/spring-vault-repository/src/test/java/org/springframework/vault/repository/query/VaultQueryCreatorUnitTests.java new file mode 100644 index 00000000..5208a171 --- /dev/null +++ b/spring-vault-repository/src/test/java/org/springframework/vault/repository/query/VaultQueryCreatorUnitTests.java @@ -0,0 +1,236 @@ +/* + * Copyright 2017 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 + * + * http://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.vault.repository.query; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit test for {@link VaultQueryCreator}. + * + * @author Mark Paluch + */ +public class VaultQueryCreatorUnitTests { + + @Test + public void greaterThan() { + + VaultQuery query = createQuery("findByIdGreaterThan", "5"); + + assertThat(query.getPredicate()).accepts("6", "7").rejects("4", "5"); + } + + @Test + public void greaterThanOrEqual() { + + VaultQuery query = createQuery("findByIdGreaterThanEqual", "5"); + + assertThat(query.getPredicate()).accepts("5", "6", "7").rejects("4"); + } + + @Test + public void lessThan() { + + VaultQuery query = createQuery("findByIdLessThan", "5"); + + assertThat(query.getPredicate()).accepts("4").rejects("5", "6", "7"); + } + + @Test + public void lessThanOrEqual() { + + VaultQuery query = createQuery("findByIdLessThanEqual", "5"); + + assertThat(query.getPredicate()).accepts("4", "5").rejects("6", "7"); + } + + @Test + public void between() { + + VaultQuery query = createQuery("findByIdIsBetween", "5", "7"); + + assertThat(query.getPredicate()).accepts("5", "6", "7").rejects("4", "8"); + } + + @Test + public void in() { + + VaultQuery query = createQuery("findByIdIn", Arrays.asList("2", "3")); + + assertThat(query.getPredicate()).accepts("2", "3").rejects("4", "8"); + } + + @Test + public void negateIn() { + + VaultQuery query = createQuery("findByIdNotIn", Arrays.asList("2", "3")); + + assertThat(query.getPredicate()).accepts("4", "8").rejects("2", "3"); + } + + @Test + public void startingWith() { + + VaultQuery query = createQuery("findByIdStartsWith", "Walter"); + + assertThat(query.getPredicate()).accepts("Walter White").rejects("Skyler"); + } + + @Test + public void endingWith() { + + VaultQuery query = createQuery("findByIdEndsWith", "White"); + + assertThat(query.getPredicate()).accepts("Walter White").rejects("Skyler"); + } + + @Test + public void containing() { + + VaultQuery query = createQuery("findByIdContaining", "er Wh"); + + assertThat(query.getPredicate()).accepts("Walter White").rejects("Skyler"); + } + + @Test + public void negateContaining() { + + VaultQuery query = createQuery("findByIdNotContaining", "er Wh"); + + assertThat(query.getPredicate()).accepts("Skyler").rejects("Walter White"); + } + + @Test + public void regex() { + + VaultQuery query = createQuery("findByIdMatches", "Wa(.*)r"); + + assertThat(query.getPredicate()).accepts("Walter", "Water").rejects("Skyler"); + } + + @Test + public void isTrue() { + + VaultQuery query = createQuery("findByIdIsTrue", ""); + + assertThat(query.getPredicate()).accepts("true", "True").rejects("false"); + } + + @Test + public void isFalse() { + + VaultQuery query = createQuery("findByIdIsFalse", ""); + + assertThat(query.getPredicate()).accepts("false", "False").rejects("true"); + } + + @Test + public void simpleProperty() { + + VaultQuery query = createQuery("findById", "Walter"); + + assertThat(query.getPredicate()).accepts("Walter").rejects("Skyler"); + } + + @Test + public void negateSimpleProperty() { + + VaultQuery query = createQuery("findByIdNot", "Walter"); + + assertThat(query.getPredicate()).accepts("Skyler").rejects("Walter"); + } + + @Test + public void greaterThanOrEquals() { + + VaultQuery query = createQuery("findByIdGreaterThanOrIdIs", "5", "2"); + + assertThat(query.getPredicate()).accepts("6", "7", "2").rejects("3", "4", "5"); + } + + @Test + public void greaterThanAndLessThan() { + + VaultQuery query = createQuery("findByIdGreaterThanAndIdLessThan", "2", "5"); + + assertThat(query.getPredicate()).accepts("3", "4").rejects("2", "5", "6"); + } + + VaultQuery createQuery(String methodName, String value) { + + DefaultParameters defaultParameters = new DefaultParameters( + ReflectionUtils.findMethod(dummy.class, "someUnrelatedMethod", + String.class)); + + PartTree partTree = new PartTree(methodName, Credentials.class); + VaultQueryCreator queryCreator = new VaultQueryCreator( + partTree, + new ParametersParameterAccessor(defaultParameters, new Object[] { value })); + + return queryCreator.createQuery().getCriteria(); + } + + VaultQuery createQuery(String methodName, List value) { + + DefaultParameters defaultParameters = new DefaultParameters( + ReflectionUtils + .findMethod(dummy.class, "someUnrelatedMethod", List.class)); + + PartTree partTree = new PartTree(methodName, Credentials.class); + VaultQueryCreator queryCreator = new VaultQueryCreator( + partTree, + new ParametersParameterAccessor(defaultParameters, new Object[] { value })); + + return queryCreator.createQuery().getCriteria(); + } + + VaultQuery createQuery(String methodName, String value, String anotherValue) { + + DefaultParameters defaultParameters = new DefaultParameters( + ReflectionUtils.findMethod(dummy.class, "someUnrelatedMethod", + String.class, String.class)); + + PartTree partTree = new PartTree(methodName, Credentials.class); + VaultQueryCreator queryCreator = new VaultQueryCreator(partTree, + new ParametersParameterAccessor(defaultParameters, new Object[] { value, + anotherValue })); + + return queryCreator.createQuery().getCriteria(); + } + + static interface dummy { + + Object someUnrelatedMethod(String arg); + + Object someUnrelatedMethod(List arg); + + Object someUnrelatedMethod(String from, String to); + } + + static class Credentials { + + String id; + } +} \ No newline at end of file diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index e27ae476..4660764e 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -6,6 +6,7 @@ * Authentication steps DSL to <>. * Reactive Vault client via `ReactiveVaultOperations`. +* <> based on Spring Data KeyValue. [[new-features.1-0-0]] === What's new in Spring Vault 1.0 diff --git a/src/main/asciidoc/reference/vault-repositories.adoc b/src/main/asciidoc/reference/vault-repositories.adoc new file mode 100644 index 00000000..17cf5430 --- /dev/null +++ b/src/main/asciidoc/reference/vault-repositories.adoc @@ -0,0 +1,276 @@ +[[vault.repositories]] += Vault Repositories + +Working with `VaultTemplate` and responses mapped to Java classes allows basic data operations like read, write +and delete. Vault repositories apply Spring Data's repository concept on top of Vault. +A Vault repository exposes basic CRUD functionality and supports query derivation with predicates constraining +the Id property, paging and sorting. + +[[vault.repositories.usage]] +== Usage + +To access domain entities stored in Vault you can leverage repository support that eases implementing those quite significantly. + +.Sample Credentials Entity +==== +[source,java] +---- +@Secret +public class Credentials { + + @Id String id; + String password; + String socialSecurityNumber; + Address address; +} +---- +==== + +We have a pretty simple domain object here. Note that it has a property named `id` annotated with +`org.springframework.data.annotation.Id` and a `@Secret` annotation on its type. +Those two are responsible for creating the actual key used to persist the object as JSON inside Vault. + +NOTE: Properties annotated with `@Id` as well as those named `id` are considered as the identifier properties. +Those with the annotation are favored over others. + +The next step is to declare a repository interface that uses the domain object. + +.Basic Repository Interface for `Credentials` entities +==== +[source,java] +---- +public interface CredentialsRepository extends CrudRepository { + +} +---- +==== + +As our repository extends `CrudRepository` it provides basic CRUD and query methods. +The thing we need in between to glue things together is the according Spring configuration. + +.JavaConfig for Vault Repositories +==== +[source,java] +---- +@Configuration +@EnableVaultRepositories +public class ApplicationConfig { + + @Bean + public VaultTemplate vaultTemplate() { + return new VaultTemplate(…); + } +} +---- +==== + +Given the setup above we can go on and inject `CredentialsRepository` into our components. + +.Access to Person Entities +==== +[source,java] +---- +@Autowired CredentialsRepository repo; + +public void basicCrudOperations() { + + Credentials creds = new Credentials("heisenberg", "327215", "AAA-GG-SSSS"); + rand.setAddress(new Address("308 Negra Arroyo Lane", "Albuquerque", "New Mexico", "87104")); + + repo.save(creds); <1> + + repo.findOne(creds.getId()); <2> + + repo.count(); <3> + + repo.delete(creds); <4> +} +---- +<1> Stores properties of `Credentials` inside Vault Hash with a key pattern `keyspace/id`, +in this case `credentials/heisenberg`, in the generic secret backend. +<2> Uses the provided id to retrieve the object stored at `keyspace/id`. +<3> Counts the total number of entities available within the keyspace _credentials_ defined by `@Secret` on `Credentials`. +<4> Removes the key for the given object from Vault. +==== + +[[vault.repositories.mapping]] +== Object to Vault JSON Mapping + +Vault repositories store objects in Vault using JSON as interchange format. Object mapping between JSON and +the entity is done by `VaultConverter`. The converter reads and writes `SecretDocument` that contains the body +from a `VaultResponse`. ``VaultResponse``s are read from Vault and the body is deserialized by +Jackson into a `Map` of `String` and `Object`. +The default `VaultConverter` implementation reads the `Map` with nested values, `List` and `Map` objects and +converts these to entities and vice versa. + +Given the `Credentials` type from the previous sections the default mapping is as follows: + +==== +[source,json] +---- +{ + "_class": "org.example.Credentials", <1> + "password", "327215", <2> + "socialSecurityNumber": "AAA-GG-SSSS", + "address": { <3> + "street": "308 Negra Arroyo Lane", + "city": "Albuquerque", + "state": "New Mexico", + "zip":"87104" + } +} +---- +<1> The `_class` attribute is included on root level as well as on any nested interface or abstract types. +<2> Simple property values are mapped by path. +<3> Properties of complex types are mapped as nested objects. +==== + +NOTE: The `@Id` property must be mapped to `String`. + +[cols="1,2,3", options="header"] +.Default Mapping Rules +|=== +| Type +| Sample +| Mapped Value + +| Simple Type + +(eg. String) +| String firstname = "Walter"; +| firstname = "Walter" + +| Complex Type + +(eg. Address) +| Address adress = new Address("308 Negra Arroyo Lane"); +| address: { "street": "308 Negra Arroyo Lane" } + +| List + +of Simple Type +| List nicknames = asList("walt", "heisenberg"); +| nicknames: ["walt", "heisenberg"] + +| Map + +of Simple Type +| Map atts = asMap("age", 51) +| atts : {"age" : 51} + +| List + +of Complex Type +| List
addresses = asList(new Address("308… +| address: [{ "street": "308 Negra Arroyo Lane" }, …] + +|=== + +You can customize the mapping behavior by registering a `Converter` in `VaultCustomConversions`. +Those converters can take care of converting from/to a type such as `LocalDate` as well as `SecretDocument` +whereas the first one is suitable for converting simple properties and the last one complex types to their JSON +representation. The second option offers full control over the resulting `SecretDocument`. Writing objects to `Vault` +will delete the content and re-create the whole entry, so not mapped data will be lost. + +[[vault.repositories.queries]] +== Queries and Query Methods + +Query methods allow automatic derivation of simple queries from the method name. Vault has no query engine but +requires direct access of HTTP context paths. Vault query methods translate Vault's API possibilities to queries. +A query method execution lists children under a context path, applies filtering to the Id, optionally limits the +Id stream with offset/limit and applies sorting after fetching the results. + +.Sample Repository Query Method +==== +[source,java] +---- +public interface CredentialsRepository extends CrudRepository { + + List findByIdStartsWith(String prefix); +} +---- +==== + +NOTE: Query methods for Vault repositories support only queries with predicates on the `@Id` property. + +Here's an overview of the keywords supported for Vault. + +[cols="1,2" options="header"] +.Supported keywords for query methods +|=== +| Keyword +| Sample + +| `After`, `GreaterThan` +| `findByIdGreaterThan(String id)` + +| `GreaterThanEqual` +| `findByIdGreaterThanEqual(String id)` + +| `Before`, `LessThan` +| `findByIdLessThan(String id)` + +| `LessThanEqual` +| `findByIdLessThanEqual(String id)` + +| `Between` +| `findByIdBetween(String from, String to)` + +| `In` +| `findByIdIn(Collection ids)` + +| `NotIn` +| `findByIdNotIn(Collection ids)` + +| `Like`, `StartingWith`, `EndingWith` +| `findByIdLike(String id)` + +| `NotLike`, `IsNotLike` +| `findByIdNotLike(String id)` + +| `Containing` +| `findByFirstnameContaining(String id)` + +| `NotContaining` +| `findByFirstnameNotContaining(String name)` + +| `Regex` +| `findByIdRegex(String id)` + +| `(No keyword)` +| `findById(String name)` + +| `Not` +| `findByIdNot(String id)` + +| `And` +| `findByLastnameAndFirstname` + +| `Or` +| `findByLastnameOrFirstname` + +| `Is,Equals` +| `findByFirstname`,`findByFirstnameIs`,`findByFirstnameEquals` + +| `Top,First` +| `findFirst10ByFirstname`,`findTop5ByFirstname` +|=== + +=== Sorting and Paging + +Query methods support sorting and paging by selecting in memory a sublist (offset/limit) Id's retrieved from +a Vault context path. Sorting has is not limited to a particular field, unlike query method predicates. +Unpaged sorting is applied after Id filtering and all resulting secrets are fetched from Vault. This way +a query method fetches only results that are also returned as part of the result. + +Using paging and sorting requires secret fetching before filtering the Id's which impacts performance. +Sorting and paging guarantees to return the same result even if the natural order of Id returned by Vault changes. +Therefore, all Id's are fetched from Vault first, then sorting is applied and afterwards filtering and offset/limiting. + +.Paging and Sorting Repository +==== +[source,java] +---- +public interface CredentialsRepository extends PagingAndSortingRepository { + + List findTop10ByIdStartsWithOrderBySocialSecurityNumberDesc(String prefix); + + List findByIdStarts(String prefix, Pageable pageRequest); +} +---- +==== diff --git a/src/main/asciidoc/reference/vault.adoc b/src/main/asciidoc/reference/vault.adoc index 36fab84c..e5714b72 100644 --- a/src/main/asciidoc/reference/vault.adoc +++ b/src/main/asciidoc/reference/vault.adoc @@ -25,6 +25,8 @@ include::reactive-template.adoc[] include::propertysource.adoc[] +include::vault-repositories.adoc[] + include::client-support.adoc[] include::authentication.adoc[]