Add support for policies.

We now support policy management via Vault's policy endpoint to enumerate policy names, read, write and delete policies. Policy parsing support is limited to JSON as there is no Java HCL parser.

Closes gh-10.
This commit is contained in:
Mark Paluch
2017-09-27 15:23:46 +02:00
parent 7ad054442c
commit 209e0b2d37
9 changed files with 1150 additions and 0 deletions

View File

@@ -165,6 +165,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>test</scope>
</dependency>
<!-- Logging -->
<dependency>

View File

@@ -15,9 +15,12 @@
*/
package org.springframework.vault.core;
import java.util.List;
import java.util.Map;
import org.springframework.lang.Nullable;
import org.springframework.vault.VaultException;
import org.springframework.vault.support.Policy;
import org.springframework.vault.support.VaultHealth;
import org.springframework.vault.support.VaultInitializationRequest;
import org.springframework.vault.support.VaultInitializationResponse;
@@ -126,6 +129,50 @@ public interface VaultSysOperations {
*/
void authUnmount(String path) throws VaultException;
/**
* Lists policy names stored in Vault.
*
* @return policy names.
* @since 2.0
* @see <a href="https://www.vaultproject.io/api/system/policy.html">GET
* /sys/policy/</a>
*/
List<String> getPolicyNames() throws VaultException;
/**
* Read a {@link Policy} by its {@literal name}. Policies are either represented as
* HCL (HashiCorp configuration language) or JSON. Retrieving policies is only
* possible if the policy is represented as JSON.
*
* @return the {@link Policy} or {@literal null}, if the policy was not found.
* @since 2.0
* @throws UnsupportedOperationException if the policy is represented as HCL.
* @see <a href="https://www.vaultproject.io/api/system/policy.html">GET
* /sys/policy/{name}</a>
*/
@Nullable
Policy getPolicy(String name) throws VaultException;
/**
* Create or update a {@link Policy}.
*
* @param name the policy name, must not be {@literal null} or empty.
* @since 2.0
* @see <a href="https://www.vaultproject.io/api/system/policy.html">PUT
* /sys/policy/{name}</a>
*/
void createOrUpdatePolicy(String name, Policy policy) throws VaultException;
/**
* Delete a {@link Policy} by its {@literal name}.
*
* @param name the policy name, must not be {@literal null} or empty.
* @since 2.0
* @see <a href="https://www.vaultproject.io/api/system/policy.html">DELETE
* /sys/policy/{name}</a>
*/
void deletePolicy(String name) throws VaultException;
/**
* Return the health status of Vault.
*

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.vault.core;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -31,15 +32,19 @@ import lombok.Data;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.Policy;
import org.springframework.vault.support.VaultHealth;
import org.springframework.vault.support.VaultInitializationRequest;
import org.springframework.vault.support.VaultInitializationResponse;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultResponse;
import org.springframework.vault.support.VaultResponseSupport;
import org.springframework.vault.support.VaultToken;
import org.springframework.vault.support.VaultUnsealStatus;
@@ -64,6 +69,8 @@ public class VaultSysTemplate implements VaultSysOperations {
private static final Health HEALTH = new Health();
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final VaultOperations vaultOperations;
/**
@@ -194,6 +201,75 @@ public class VaultSysTemplate implements VaultSysOperations {
vaultOperations.delete(String.format("sys/auth/%s", path));
}
@Override
@SuppressWarnings("unchecked")
public List<String> getPolicyNames() throws VaultException {
return requireResponse((List<String>) vaultOperations.read("sys/policy")
.getRequiredData().get("policies"));
}
@Nullable
@Override
public Policy getPolicy(String name) throws VaultException {
Assert.hasText(name, "Name must not be null or empty");
return vaultOperations.doWithSession(restOperations -> {
ResponseEntity<VaultResponse> response = restOperations.getForEntity(
"sys/policy/{name}", VaultResponse.class, name);
if (response.getStatusCode() == HttpStatus.NOT_FOUND) {
return null;
}
String rules = (String) response.getBody().getRequiredData().get("rules");
if (StringUtils.isEmpty(rules)) {
return Policy.empty();
}
if (rules.trim().startsWith("{")) {
return VaultResponses.unwrap(rules, Policy.class);
}
throw new UnsupportedOperationException("Cannot parse policy in HCL format");
});
}
@Override
public void createOrUpdatePolicy(String name, Policy policy) throws VaultException {
Assert.hasText(name, "Name must not be null or empty");
Assert.notNull(policy, "Policy must not be null");
String rules;
try {
rules = OBJECT_MAPPER.writeValueAsString(policy);
}
catch (IOException e) {
throw new VaultException("Cannot serialize policy to JSON", e);
}
vaultOperations.doWithSession(restOperations -> {
restOperations.exchange("sys/policy/{name}", HttpMethod.PUT,
new HttpEntity<>(Collections.singletonMap("rules", rules)),
VaultResponse.class, name);
return null;
});
}
@Override
public void deletePolicy(String name) throws VaultException {
Assert.hasText(name, "Name must not be null or empty");
vaultOperations.delete(String.format("sys/policy/%s", name));
}
@Override
public VaultHealth health() {
return requireResponse(vaultOperations.doWithVault(HEALTH));

View File

@@ -0,0 +1,758 @@
/*
* 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.support;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.util.Converter;
import lombok.EqualsAndHashCode;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.support.Policy.PolicyDeserializer;
import org.springframework.vault.support.Policy.PolicySerializer;
/**
* Value object representing a Vault policy associated with {@link Rule}s. Instances of
* {@link Policy} support JSON serialization and deserialization using Jackson.
*
* @author Mark Paluch
* @since 2.0
* @see Rule
* @see com.fasterxml.jackson.databind.ObjectMapper
*/
@JsonSerialize(using = PolicySerializer.class)
@JsonDeserialize(using = PolicyDeserializer.class)
@EqualsAndHashCode
public class Policy {
private static final Policy EMPTY = new Policy(Collections.emptySet());
private final Set<Rule> rules;
private Policy(Set<Rule> rules) {
this.rules = rules;
}
/**
* Create an empty {@link Policy} without rules.
*
* @return an empty {@link Policy}.
*/
public static Policy empty() {
return EMPTY;
}
/**
* Create a {@link Policy} from one or more {@code rules}.
*
* @param rules must not be {@literal null}.
* @return the {@link Policy} object containing {@code rules}.
*/
public static Policy of(Rule... rules) {
Assert.notNull(rules, "Rules must not be null");
Assert.noNullElements(rules, "Rules must not contain null elements");
return new Policy(new LinkedHashSet<>(Arrays.asList(rules)));
}
/**
* Create a {@link Policy} from one or more {@code rules}.
*
* @param rules must not be {@literal null}.
* @return the {@link Policy} object containing {@code rules}.
*/
public static Policy of(Set<Rule> rules) {
Assert.notNull(rules, "Rules must not be null");
return new Policy(new LinkedHashSet<>(rules));
}
/**
* Create a new {@link Policy} object containing all configured rules and add the
* given {@link Rule} to the new policy object. If the given {@link Rule} matches an
* existing rule path, the exiting rule will be overridden by the new rule object.
*
* @param rule must not be {@literal null}.
* @return the new {@link Policy} object containing all configured rules and the given
* {@link Rule}.
*/
public Policy with(Rule rule) {
Assert.notNull(rule, "Rule must not be null");
Set<Rule> rules = new LinkedHashSet<>(this.rules.size() + 1);
rules.addAll(this.rules);
rules.add(rule);
return new Policy(rules);
}
public Set<Rule> getRules() {
return rules;
}
/**
* Lookup a {@link Rule} by its path. Returns {@literal null} if the rule was not
* found.
*
* @param path must not be {@literal null}.
* @return the {@link Rule} or {@literal null}, if not found.
*/
@Nullable
public Rule getRule(String path) {
Assert.notNull(path, "Path must not be null");
for (Rule rule : rules) {
if (rule.getPath().equals(path)) {
return rule;
}
}
return null;
}
/**
* Value object representing a rule for a certain path. Rule equality is considered by
* comparing only the path segment to guarante uniqueness within a {@link Set}.
*
* @author Mark Paluch
*/
@EqualsAndHashCode(of = "path")
@JsonInclude(Include.NON_EMPTY)
public static class Rule {
/**
* Path or path with asterisk to which this rule applies to.
*/
@JsonIgnore
private final String path;
/**
* One or more capabilities which provide fine-grained control over permitted (or
* denied) operations.
*/
@JsonSerialize(contentConverter = CapabilityToStringConverter.class)
@JsonDeserialize(contentConverter = StringToCapabilityConverter.class)
private final List<Capability> capabilities;
/**
* The minimum allowed TTL that clients can specify for a wrapped response. In
* practice, setting a minimum TTL of one second effectively makes response
* wrapping mandatory for a particular path.
*/
@JsonProperty("min_wrapping_ttl")
@JsonSerialize(converter = DurationToStringConverter.class)
@Nullable
private final Duration minWrappingTtl;
/**
* The maximum allowed TTL that clients can specify for a wrapped response.
*/
@JsonProperty("max_wrapping_ttl")
@JsonSerialize(converter = DurationToStringConverter.class)
@Nullable
private final Duration maxWrappingTtl;
/**
* Whitelists a list of keys and values that are permitted on the given path.
* Setting a parameter with a value of a populated list allows the parameter to
* contain only those values.
*/
@JsonProperty("allowed_parameters")
private final Map<String, List<String>> allowedParameters;
/**
* Blacklists a list of parameter and values. Any values specified here take
* precedence over {@link #allowedParameters}. Setting a parameter with a value of
* a populated list denies any parameter containing those values. Setting to
* {@literal *} will deny any parameter.
*/
@JsonProperty("denied_parameters")
private final Map<String, List<String>> deniedParameters;
@JsonCreator
private Rule(
@JsonProperty("capabilities") List<Capability> capabilities,
@JsonProperty("min_wrapping_ttl") @JsonDeserialize(converter = StringToDurationConverter.class) Duration minWrappingTtl,
@JsonProperty("max_wrapping_ttl") @JsonDeserialize(converter = StringToDurationConverter.class) Duration maxWrappingTtl,
@JsonProperty("allowed_parameters") Map<String, List<String>> allowedParameters,
@JsonProperty("denied_parameters") Map<String, List<String>> deniedParameters) {
this.path = "";
this.capabilities = capabilities;
this.minWrappingTtl = minWrappingTtl;
this.maxWrappingTtl = maxWrappingTtl;
this.allowedParameters = allowedParameters;
this.deniedParameters = deniedParameters;
}
private Rule(String path, List<Capability> capabilities,
@Nullable Duration minWrappingTtl, @Nullable Duration maxWrappingTtl,
Map<String, List<String>> allowedParameters,
Map<String, List<String>> deniedParameters) {
this.path = path;
this.capabilities = capabilities;
this.minWrappingTtl = minWrappingTtl;
this.maxWrappingTtl = maxWrappingTtl;
this.allowedParameters = allowedParameters;
this.deniedParameters = deniedParameters;
}
private Rule withPath(String path) {
return new Rule(path, capabilities, minWrappingTtl, maxWrappingTtl,
allowedParameters, deniedParameters);
}
public String getPath() {
return path;
}
public List<Capability> getCapabilities() {
return capabilities;
}
@Nullable
public Duration getMinWrappingTtl() {
return minWrappingTtl;
}
@Nullable
public Duration getMaxWrappingTtl() {
return maxWrappingTtl;
}
public Map<String, List<String>> getAllowedParameters() {
return allowedParameters;
}
public Map<String, List<String>> getDeniedParameters() {
return deniedParameters;
}
/**
* Create a new builder for {@link Rule}.
*
* @return a new {@link RuleBuilder}.
*/
public static RuleBuilder builder() {
return new RuleBuilder();
}
/**
* Builder for a {@link Rule}.
*/
public static class RuleBuilder {
private @Nullable String path;
private Set<Capability> capabilities = new LinkedHashSet<>();
@Nullable
private Duration minWrappingTtl;
@Nullable
private Duration maxWrappingTtl;
private Map<String, List<String>> allowedParameters = new LinkedHashMap<String, List<String>>();
private Map<String, List<String>> deniedParameters = new LinkedHashMap<String, List<String>>();;
/**
* Associate a {@code path} with the rule.
*
* @param path must not be {@literal null} or empty.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder path(String path) {
Assert.hasText(path, "Path must not be empty");
this.path = path;
return this;
}
/**
* Configure a {@link Capability} for the rule. Capabilities are added when
* calling this method and do not replace already configured capabilities.
*
* @param capability must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder capability(Capability capability) {
Assert.notNull(capability, "Capability must not be null");
this.capabilities.add(capability);
return this;
}
/**
* Configure capabilities. apabilities are added when calling this method and
* do not replace already configured capabilities.
*
* @param capabilities must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder capabilities(Capability... capabilities) {
Assert.notNull(capabilities, "Capabilities must not be null");
Assert.noNullElements(capabilities,
"Capabilities must not contain null elements");
return capabilities(Arrays.asList(capabilities));
}
/**
* Configure capabilities represented as {@link String} literals. This method
* resolves capabilities using {@link BuiltinCapabilities}. Capabilities are
* added when calling this method and do not replace already configured
* capabilities.
*
* @param capabilities must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
* @throws IllegalArgumentException if the capability cannot be resolved to a
* built-in {@link Capability}.
*/
public RuleBuilder capabilities(String... capabilities) {
Assert.notNull(capabilities, "Capabilities must not be null");
Assert.noNullElements(capabilities,
"Capabilities must not contain null elements");
List<Capability> mapped = Arrays
.stream(capabilities)
.map(value -> {
Capability capability = BuiltinCapabilities.find(value);
if (capability == null) {
throw new IllegalArgumentException("Cannot resolve "
+ value + " to a capability");
}
return capability;
}).collect(Collectors.toList());
return capabilities(mapped);
}
private RuleBuilder capabilities(Iterable<Capability> capabilities) {
for (Capability capability : capabilities) {
this.capabilities.add(capability);
}
return this;
}
/**
* Configure a min TTL for response wrapping.
*
* @param ttl must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder minWrappingTtl(Duration ttl) {
Assert.notNull(ttl, "TTL must not be null");
this.minWrappingTtl = ttl;
return this;
}
/**
* Configure a max TTL for response wrapping.
*
* @param ttl must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder maxWrappingTtl(Duration ttl) {
Assert.notNull(ttl, "TTL must not be null");
this.maxWrappingTtl = ttl;
return this;
}
/**
* Configure allowed parameter values given {@code name} and {@code values}.
* Allowing parameter values replaces previously configured allowed parameter
* values. Empty {@code values} allow all values for the given parameter
* {@code name}.
*
* @param name must not be {@literal null} or empty.
* @param values must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder allowedParameter(String name, String... values) {
Assert.hasText(name, "Allowed parameter name must not be empty");
Assert.notNull(values, "Values must not be null");
this.allowedParameters.put(name, Arrays.asList(values));
return this;
}
/**
* Configure denied parameter values given {@code name} and {@code values}.
* Denying parameter values replaces previously configured denied parameter
* values. Empty {@code values} deny parameter usage.
*
* @param name must not be {@literal null} or empty.
* @param values must not be {@literal null}.
* @return {@code this} {@link RuleBuilder}.
*/
public RuleBuilder deniedParameter(String name, String... values) {
Assert.hasText(name, "Denied parameter name must not be empty");
Assert.notNull(values, "Values must not be null");
this.deniedParameters.put(name, Arrays.asList(values));
return this;
}
/**
* Build the {@link Rule} object. Requires a configured {@link #path(String)}
* and at least one {@link #capability(Capability)}.
*
* @return the new {@link Rule} object.
*/
public Rule build() {
Assert.state(StringUtils.hasText(path), "Path must not be empty");
Assert.state(!capabilities.isEmpty(),
"Rule must define one or more capabilities");
List<Capability> capabilities;
switch (this.capabilities.size()) {
case 0:
capabilities = Collections.emptyList();
break;
case 1:
capabilities = Collections.singletonList(this.capabilities.iterator()
.next());
break;
default:
capabilities = Collections.unmodifiableList(new ArrayList<>(
this.capabilities));
}
return new Rule(path, capabilities, minWrappingTtl, maxWrappingTtl,
createMap(this.allowedParameters),
createMap(this.deniedParameters));
}
private Map<String, List<String>> createMap(Map<String, List<String>> source) {
if (source.isEmpty()) {
return Collections.emptyMap();
}
return Collections.unmodifiableMap(new LinkedHashMap<>(source));
}
}
}
/**
* Capability interface representing capability literals.
*/
public interface Capability {
/**
* @return the capability literal.
*/
String name();
}
/**
* Built-in Vault capabilities.
*/
public enum BuiltinCapabilities implements Capability {
/**
* Allows creating data at the given path. Very few parts of Vault distinguish
* between create and update, so most operations require both create and update
* capabilities.
*/
CREATE,
/**
* Allows reading the data at the given path.
*/
READ,
/**
* Allows change the data at the given path. In most parts of Vault, this
* implicitly includes the ability to create the initial value at the path.
*/
UPDATE,
/**
* Deprecated: Previous capability literal before it was split into
* {@link #CREATE} and {@link #UPDATE}.
*/
WRITE,
/**
* Allows deleting the data at the given path.
*/
DELETE,
/**
* Allows listing values at the given path. Note that the keys returned by a list
* operation are not filtered by policies. Do not encode sensitive information in
* key names. Not all backends support listing.
*/
LIST,
/**
* Allows access to paths that are root-protected. Tokens are not permitted to
* interact with these paths unless they are have the sudo capability (in addition
* to the other necessary capabilities for performing an operation against that
* path, such as read or delete).
*/
SUDO,
/**
* Disallows access. This always takes precedence regardless of any other defined
* capabilities, including {@link #SUDO}.
*/
DENY;
/**
* Find a {@link Capability} by its name. The name is compared case-insensitive.
*
* @param value must not be {@literal null}.
* @return the {@link Capability} or {@literal null}, if not found.
*/
@Nullable
public static Capability find(String value) {
for (BuiltinCapabilities cap : values()) {
if (cap.name().equalsIgnoreCase(value)) {
return cap;
}
}
return null;
}
}
static class PolicySerializer extends JsonSerializer<Policy> {
@Override
public void serialize(Policy value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeFieldName("path");
gen.writeStartObject();
for (Rule rule : value.getRules()) {
gen.writeObjectField(rule.path, rule);
}
gen.writeEndObject();
gen.writeEndObject();
}
}
static class PolicyDeserializer extends JsonDeserializer<Policy> {
@Override
public Policy deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT,
"Expected START_OBJECT, got: " + p.getCurrentToken());
String fieldName = p.nextFieldName();
Set<Rule> rules = new LinkedHashSet<>();
if ("path".equals(fieldName)) {
p.nextToken();
Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT,
"Expected START_OBJECT, got: " + p.getCurrentToken());
p.nextToken();
while (p.currentToken() == JsonToken.FIELD_NAME) {
String path = p.getCurrentName();
p.nextToken();
Assert.isTrue(p.getCurrentToken() == JsonToken.START_OBJECT,
"Expected START_OBJECT, got: " + p.getCurrentToken());
Rule rule = p.getCodec().readValue(p, Rule.class);
rules.add(rule.withPath(path));
JsonToken jsonToken = p.nextToken();
if (jsonToken == JsonToken.END_OBJECT) {
break;
}
}
Assert.isTrue(p.getCurrentToken() == JsonToken.END_OBJECT,
"Expected END_OBJECT, got: " + p.getCurrentToken());
p.nextToken();
}
Assert.isTrue(p.getCurrentToken() == JsonToken.END_OBJECT,
"Expected END_OBJECT, got: " + p.getCurrentToken());
return Policy.of(rules);
}
}
static class CapabilityToStringConverter implements Converter<Capability, String> {
@Override
public String convert(Capability value) {
return value.name().toLowerCase();
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(Capability.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(String.class);
}
}
static class StringToCapabilityConverter implements Converter<String, Capability> {
@Override
public Capability convert(String value) {
Capability capability = BuiltinCapabilities.find(value);
return capability != null ? capability : () -> value;
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(String.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(Capability.class);
}
}
static class DurationToStringConverter implements Converter<Duration, String> {
@Override
public String convert(Duration value) {
return "" + value.getSeconds();
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(Duration.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(String.class);
}
}
static class StringToDurationConverter implements Converter<String, Duration> {
static Pattern SECONDS = Pattern.compile("(\\d+)s");
static Pattern MINUTES = Pattern.compile("(\\d+)m");
static Pattern HOURS = Pattern.compile("(\\d+)h");
@Override
public Duration convert(String value) {
try {
return Duration.ofSeconds(Long.parseLong(value));
}
catch (NumberFormatException e) {
Matcher matcher = SECONDS.matcher(value);
if (matcher.matches()) {
return Duration.ofSeconds(Long.parseLong(matcher.group(1)));
}
matcher = MINUTES.matcher(value);
if (matcher.matches()) {
return Duration.ofMinutes(Long.parseLong(matcher.group(1)));
}
matcher = HOURS.matcher(value);
if (matcher.matches()) {
return Duration.ofHours(Long.parseLong(matcher.group(1)));
}
throw new IllegalArgumentException("Unsupported duration value: " + value);
}
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(String.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(Capability.class);
}
}
}

View File

@@ -111,7 +111,9 @@ public class VaultCertificateRequest {
@Nullable
private String commonName;
private List<String> altNames = new ArrayList<>();
private List<String> ipSubjectAltNames = new ArrayList<>();
@Nullable

View File

@@ -15,8 +15,10 @@
*/
package org.springframework.vault.core;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Before;
@@ -26,11 +28,17 @@ import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.vault.support.Policy;
import org.springframework.vault.support.VaultMount;
import org.springframework.vault.support.VaultUnsealStatus;
import org.springframework.vault.support.Policy.Rule;
import org.springframework.vault.util.IntegrationTestSupport;
import org.springframework.vault.util.Version;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assume.assumeTrue;
import static org.springframework.vault.support.Policy.BuiltinCapabilities.READ;
import static org.springframework.vault.support.Policy.BuiltinCapabilities.UPDATE;
/**
* Integration tests for {@link VaultSysTemplate} through {@link VaultSysOperations}.
@@ -44,10 +52,15 @@ public class VaultSysTemplateIntegrationTests extends IntegrationTestSupport {
@Autowired
private VaultOperations vaultOperations;
private Version vaultVersion;
private VaultSysOperations adminOperations;
@Before
public void before() throws Exception {
vaultVersion = prepare().getVersion();
adminOperations = vaultOperations.opsForSys();
}
@@ -120,6 +133,65 @@ public class VaultSysTemplateIntegrationTests extends IntegrationTestSupport {
assertThat(secret.getType()).isEqualTo("userpass");
}
@Test
public void shouldEnumeratePolicyNames() {
assumeTrue(vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.6.1")));
List<String> policyNames = adminOperations.getPolicyNames();
assertThat(policyNames).contains("root", "default");
}
@Test
public void shouldReadRootPolicy() {
assumeTrue(vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.6.1")));
Policy root = adminOperations.getPolicy("root");
assertThat(root).isEqualTo(Policy.empty());
}
@Test(expected = UnsupportedOperationException.class)
public void shouldReadDefaultPolicy() {
assumeTrue(vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.6.1")));
adminOperations.getPolicy("default");
}
@Test
public void shouldCreatePolicy() {
assumeTrue(vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.7.0")));
Rule rule = Rule.builder().path("foo").capabilities(READ, UPDATE)
.minWrappingTtl(Duration.ofSeconds(100))
.maxWrappingTtl(Duration.ofHours(2)).build();
adminOperations.createOrUpdatePolicy("foo", Policy.of(rule));
assertThat(adminOperations.getPolicyNames()).contains("foo");
Policy loaded = adminOperations.getPolicy("foo");
assertThat(loaded.getRules()).contains(rule);
}
@Test
public void shouldDeletePolicy() {
assumeTrue(vaultVersion.isGreaterThanOrEqualTo(Version.parse("0.6.0")));
Rule rule = Rule.builder().path("foo").capabilities(READ).build();
adminOperations.createOrUpdatePolicy("foo", Policy.of(rule));
adminOperations.deletePolicy("foo");
assertThat(adminOperations.getPolicyNames()).doesNotContain("foo");
}
@Test
public void isInitializedShouldReturnTrue() {
assertThat(adminOperations.isInitialized()).isTrue();
@@ -129,6 +201,7 @@ public class VaultSysTemplateIntegrationTests extends IntegrationTestSupport {
public void getUnsealStatusShouldReturnStatus() {
VaultUnsealStatus unsealStatus = adminOperations.getUnsealStatus();
assertThat(unsealStatus.isSealed()).isFalse();
assertThat(unsealStatus.getProgress()).isEqualTo(0);
}

View File

@@ -0,0 +1,146 @@
/*
* 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.support;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.StreamUtils;
import org.springframework.vault.support.Policy.Rule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link Policy} JSON serialization/deserialization.
*
* @author Mark Paluch
*/
public class PolicySerializationUnitTests {
ObjectMapper objectMapper = new ObjectMapper();
@Test
public void shouldSerialize() throws Exception {
Rule rule = Rule.builder().path("secret/*")
.capabilities("create", "read", "update")
.allowedParameter("ttl", "1h", "2h").deniedParameter("password").build();
Rule another = Rule.builder().path("secret/foo")
.capabilities("create", "read", "update", "delete", "list")
.minWrappingTtl(Duration.ofMinutes(1))
.maxWrappingTtl(Duration.ofHours(1)).allowedParameter("ttl", "1h", "2h")
.deniedParameter("password").build();
Policy policy = Policy.of(rule, another);
try (InputStream is = new ClassPathResource("policy.json").getInputStream()) {
String expected = StreamUtils.copyToString(is, StandardCharsets.UTF_8);
JSONAssert.assertEquals(expected, objectMapper.writeValueAsString(policy),
JSONCompareMode.STRICT);
}
}
@Test
public void shouldDeserialize() throws Exception {
Rule rule = Rule.builder().path("secret/*")
.capabilities("create", "read", "update", "update")
.allowedParameter("ttl", "1h", "2h").deniedParameter("password").build();
Rule another = Rule.builder().path("secret/foo")
.capabilities("create", "read", "update", "delete", "list")
.minWrappingTtl(Duration.ofMinutes(1))
.maxWrappingTtl(Duration.ofHours(1)).allowedParameter("ttl", "1h", "2h")
.allowedParameter("ttl", "1h", "2h").deniedParameter("password").build();
Policy expected = Policy.of(rule, another);
try (InputStream is = new ClassPathResource("policy.json").getInputStream()) {
Policy actual = objectMapper.readValue(is, Policy.class);
assertThat(actual.getRules()).hasSameClassAs(expected.getRules());
Rule secretAll = actual.getRule("secret/*");
assertThat(secretAll.getPath()).isEqualTo(rule.getPath());
assertThat(secretAll.getCapabilities()).isEqualTo(rule.getCapabilities());
assertThat(secretAll.getAllowedParameters()).isEqualTo(
rule.getAllowedParameters());
assertThat(secretAll.getDeniedParameters()).isEqualTo(
rule.getDeniedParameters());
Rule secretFoo = actual.getRule("secret/foo");
assertThat(secretFoo.getPath()).isEqualTo(another.getPath());
assertThat(secretFoo.getCapabilities()).isEqualTo(another.getCapabilities());
assertThat(secretFoo.getMinWrappingTtl()).isEqualTo(
another.getMinWrappingTtl());
assertThat(secretFoo.getMaxWrappingTtl()).isEqualTo(
another.getMaxWrappingTtl());
assertThat(secretFoo.getAllowedParameters()).isEqualTo(
another.getAllowedParameters());
assertThat(secretFoo.getDeniedParameters()).isEqualTo(
another.getDeniedParameters());
}
}
@Test
public void shouldDeserializeEmptyPolicy() throws Exception {
assertThat(objectMapper.readValue("{}", Policy.class)).isEqualTo(Policy.empty());
}
@Test
public void shouldRejectUnknownFieldNames() throws Exception {
assertThatThrownBy(
() -> objectMapper.readValue("{\"foo\":1, \"path\": {} }", Policy.class))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(
() -> objectMapper.readValue("{\"foo\":\"bar\"}", Policy.class))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void shouldDeserializePolicyWithEmptyRules() throws Exception {
Policy actual = objectMapper.readValue("{ \"path\": {} }", Policy.class);
assertThat(actual).isEqualTo(Policy.empty());
}
@Test
public void shouldDeserializeRuleWithHour() throws Exception {
Policy actual = objectMapper.readValue(
"{ \"path\": { \"secret\" : {\"min_wrapping_ttl\":\"1h\"} } }",
Policy.class);
Rule rule = actual.getRule("secret");
assertThat(rule.getMinWrappingTtl()).isEqualTo(Duration.ofHours(1));
}
}

View File

@@ -0,0 +1,40 @@
{
"path": {
"secret/*": {
"capabilities": [
"create",
"read",
"update"
],
"allowed_parameters": {
"ttl": [
"1h",
"2h"
]
},
"denied_parameters": {
"password": []
}
},
"secret/foo": {
"capabilities": [
"create",
"read",
"update",
"delete",
"list"
],
"min_wrapping_ttl": "60",
"max_wrapping_ttl": "3600",
"allowed_parameters": {
"ttl": [
"1h",
"2h"
]
},
"denied_parameters": {
"password": []
}
}
}
}

View File

@@ -8,6 +8,7 @@
* Reactive Vault client via `ReactiveVaultOperations`.
* <<vault.repositories,Vault repository support>> based on Spring Data KeyValue.
* Transit batch encrypt and decrypt support.
* Policy management for policies stored as JSON.
[[new-features.1-0-0]]
=== What's new in Spring Vault 1.0