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 identifier property, paging and sorting. Vault repositories use the key/value secrets engine functionality to persist and query data. As of version 2.4, Spring Vault can use additionally key/value version 2 secrets engine, the actual secrets engine version is discovered during runtime.

Deletes within versioned key/value secrets engine use the DELETE operation. Secrets are not destroyed through CrudRepository.delete(…).
Read more about Spring Data Repositories in the Spring Data Commons reference documentation. The reference documentation will give you an introduction to Spring Data repositories.

Usage

To access domain entities stored in Vault you can leverage repository support that eases implementing those quite significantly.

Example 1. Sample Credentials Entity
@Secret
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.

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.

Example 2. Basic Repository Interface for Credentials entities
interface CredentialsRepository extends CrudRepository<Credentials, String> {

}

As our repository extends CrudRepository it provides basic CRUD and query methods. Vault repositories require Spring Data components. Make sure to include spring-data-commons and spring-data-keyvalue artifacts in your class path.

The easiest way to achieve this, is by setting up dependency management and adding the artifacts to your pom.xml:

Then add the following to pom.xml dependencies section.

Example 3. Using the Spring Data BOM
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-bom</artifactId>
      <version>2023.1.2</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>

  <!-- other dependency elements omitted -->

  <dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>
    <version>3.1.1</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-keyvalue</artifactId>
    <!-- Version inherited from the BOM -->
  </dependency>

</dependencies>

The thing we need in between to glue things together is the according Spring configuration.

Example 4. JavaConfig for Vault Repositories
@Configuration
@EnableVaultRepositories
class ApplicationConfig {

  @Bean
  VaultTemplate vaultTemplate() {
    return new VaultTemplate(…);
  }
}

Given the setup above we can go on and inject CredentialsRepository into our components.

Example 5. Access to Person Entities
@Autowired CredentialsRepository repo;

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 key-value secret secrets engine.
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.

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. VaultResponses 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:

{
  "_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.
The @Id property must be mapped to String.
Table 1. 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<String> nicknames = asList("walt", "heisenberg");

"nicknames": ["walt", "heisenberg"]

Map
of Simple Type

Map<String, Integer> atts = asMap("age", 51)

"atts" : {"age" : 51}

List
of Complex Type

List<Address> 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.

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.

Example 6. Sample Repository Query Method
interface CredentialsRepository extends CrudRepository<Credentials, String> {

  List<Credentials> findByIdStartsWith(String prefix);
}
Query methods for Vault repositories support only queries with predicates on the @Id property.

Here’s an overview of the keywords supported for Vault.

Table 2. 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.

Example 7. Paging and Sorting Repository
interface CredentialsRepository extends PagingAndSortingRepository<Credentials, String> {

  List<Credentials> findTop10ByIdStartsWithOrderBySocialSecurityNumberDesc(String prefix);

  List<Credentials> findByIdStarts(String prefix, Pageable pageRequest);
}

Optimistic Locking

Vaults key/value secrets engine version 2 can maintain versioned secrets. Spring Vault supports versioning through a version property in the domain model that are annotated with @Version. Using optimistic locking makes sure updates are only applied to secrets with a matching version. Therefore, the actual value of the version property is added to the update request through the cas property. If another operation altered the secret in the meantime, then an OptimisticLockingFailureException is thrown and the secret isn’t updated.

Version properties must be numeric properties such as int or long and map to the cas property when updating secrets.

Example 8. Sample Versioned Entity
@Secret
class VersionedCredentials {

  @Id String id;
  @Version int version;
  String password;
  String socialSecurityNumber;
  Address address;
}

The following example shows these features:

Example 9. Sample Versioned Entity
VersionedCredentialsRepository repo = …;

VersionedCredentials credentials = repo.findById("sample-credentials").get();    (1)

VersionedCredentials concurrent = repo.findById("sample-credentials").get();     (2)

credentials.setPassword("something-else");

repos.save(credentials);                                                         (3)


concurrent.setPassword("concurrent change");

repos.save(concurrent); // throws OptimisticLockingFailureException              (4)
1 Obtain a secret by its Id sample-credentials.
2 Obtain a second instance of the secret by its Id sample-credentials.
3 Update the secret and let Vault increment the version.
4 Update the second instance that uses the previous version. The operation fails with an OptimisticLockingFailureException as the version was incremented in Vault in the meantime.
When deleting versioned secrets, delete by Id deletes the most recent secret. Delete by entity deletes the secret at the provided version.

Accessing versioned secrets

Key/Value version 2 secrets engine maintains versions of secrets that can be accessed by implementing RevisionRepository in your Vault repository interface declaration. Revision repositories define lookup methods to obtain revisions for a particular identifier. Identifiers must be String.

Example 10. Implementing RevisionRepository
interface RevisionCredentialsRepository extends CrudRepository<Credentials, String>,
                                        RevisionRepository<Credentials, String, Integer> (1)
{

}
1 The first type parameter (Credentials) denotes the entity type, the second (String) denotes the type of the id property, and the last one (Integer) is the type of the revision number. Vault supports only String identifiers and Integer revision numbers.

Usage

You can now use the methods from RevisionRepository to query the revisions of the entity, as the following example shows:

Example 11. Using RevisionRepository
RevisionCredentialsRepository repo = …;

Revisions<Integer, Credentials> revisions = repo.findRevisions("my-secret-id");

Page<Revision<Integer, Credentials>> firstPageOfRevisions = repo.findRevisions("my-secret-id", Pageable.ofSize(4));