Add app-id authentication mechanism.

Support app-id authentication using app id and user id. User-ids are generated either from the SHA-256 hex-encoded IP address or using the Mac address represented as hex without separators. Network addresses are obtained from the InetAddress.getLocalHost() method.

Tests use a test @Rule VaultRule to ensure Vault is unsealed and provides a persistent token that can be used as root token during tests. The token is static so it can be reused across multiple test runs without restarting vault. Add tests for generic secret backend usage with a static token and app-id authentication. Add tests for app-id authentication using IP address and Mac-address user-ids.
This commit is contained in:
Mark Paluch
2016-03-22 17:25:50 +01:00
parent 69eb7197da
commit b5ab155bcc
27 changed files with 1777 additions and 30 deletions

View File

@@ -49,6 +49,12 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.3.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2016 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.cloud.vault;
/**
* @author Mark Paluch
*/
public interface AppIdUserIdMechanism {
/**
* Creates a UserId for AppId authentication.
*
* @return
*/
String createUserId();
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 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.cloud.vault;
import java.io.IOException;
import java.net.InetAddress;
/**
* Mechanism to generate a SHA-256 hashed and hex-encoded representation of the IP address. Can be calculated with
* {@code echo -n 192.168.99.1 | sha256sum}.
*
* @author Mark Paluch
*/
public class IpAddressUserId implements AppIdUserIdMechanism {
@Override
public String createUserId() {
try {
return Sha256.toSha256(InetAddress.getLocalHost().getHostAddress());
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2016 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.cloud.vault;
import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.util.StringUtils;
/**
* Mechanism to generate a UserId based on the Mac address. {@link MacAddressUserId} creates a hex-encoded
* representation of the Mac address without any separators (0123456789AB). A
* {@link org.springframework.cloud.vault.VaultProperties.AppIdProperties#networkInterface} can be
* specified optionally to select a network interface (index/name).
*
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor
@CommonsLog
public class MacAddressUserId implements AppIdUserIdMechanism {
private final VaultProperties vaultProperties;
@Override
public String createUserId() {
try {
NetworkInterface networkInterface = null;
List<NetworkInterface> interfaces = Collections.list(NetworkInterface.getNetworkInterfaces());
VaultProperties.AppIdProperties appId = vaultProperties.getAppId();
if (StringUtils.hasText(appId.getNetworkInterface())) {
try {
networkInterface = getNetworkInterface(Integer.parseInt(appId.getNetworkInterface()), interfaces);
} catch (NumberFormatException e) {
networkInterface = getNetworkInterface((appId.getNetworkInterface()), interfaces);
}
}
if (networkInterface == null) {
if (StringUtils.hasText(appId.getNetworkInterface())) {
log.warn(
String.format("Did not find a NetworkInterface applying hint %s", appId.getNetworkInterface()));
}
InetAddress localHost = InetAddress.getLocalHost();
networkInterface = NetworkInterface.getByInetAddress(localHost);
if (networkInterface == null) {
throw new IllegalStateException(String.format("Cannot determine NetworkInterface for %s", localHost));
}
}
byte[] mac = networkInterface.getHardwareAddress();
if (mac == null) {
throw new IllegalStateException(String.format("Network interface %s has no hardware address", networkInterface.getName()));
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mac.length; i++) {
sb.append(String.format("%02X", mac[i]));
}
return Sha256.toSha256(sb.toString());
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private NetworkInterface getNetworkInterface(Number hint, List<NetworkInterface> interfaces) {
if (interfaces.size() > hint.intValue() && hint.intValue() >= 0) {
return interfaces.get(hint.intValue());
}
return null;
}
private NetworkInterface getNetworkInterface(String hint, List<NetworkInterface> interfaces) {
for (NetworkInterface anInterface : interfaces) {
if (hint.equals(anInterface.getDisplayName()) || hint.equals(anInterface.getName())) {
return anInterface;
}
}
return null;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2016 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.cloud.vault;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.security.crypto.codec.Hex;
import org.springframework.util.Assert;
/**
* Utility to generate SHA 256 checksums.
*
* @author Mark Paluch
*/
class Sha256 {
/**
* Generates a hex-encoded SHA256 checksum from the supplied {@code content}.
*
* @param content
* @return
*/
public static String toSha256(String content) {
Assert.hasText(content, "Content must not be empty!");
MessageDigest messageDigest = getMessageDigest("SHA-256");
byte[] digest = messageDigest.digest(content.getBytes(StandardCharsets.US_ASCII));
return new String(Hex.encode(digest));
}
/**
* Get a MessageDigest instance for the given algorithm. Throws an IllegalArgumentException if <i>algorithm</i> is
* unknown
*
* @return MessageDigest instance
* @throws IllegalArgumentException if NoSuchAlgorithmException is thrown
*/
private static MessageDigest getMessageDigest(String algorithm) throws IllegalArgumentException {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm [" + algorithm + "]");
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2016 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.cloud.vault;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.apachecommons.CommonsLog;
/**
* A static UserId.
* @author Mark Paluch
*/
@Value
@RequiredArgsConstructor
@CommonsLog
public class StaticUserId implements AppIdUserIdMechanism {
private final VaultProperties vaultProperties;
@Override
public String createUserId() {
return vaultProperties.getAppId().getUserId();
}
}

View File

@@ -16,13 +16,19 @@
package org.springframework.cloud.vault;
import java.util.Map;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
/**
* @author Spencer Gibb
* @author Mark Paluch
*/
@Configuration
@EnableConfigurationProperties
@@ -30,8 +36,16 @@ import org.springframework.context.annotation.Configuration;
public class VaultBootstrapConfiguration {
@Bean
public VaultClient vaultClient() {
return new VaultClient(vaultProperties());
public VaultClient vaultClient(ApplicationContext applicationContext) {
VaultClient vaultClient = new VaultClient(vaultProperties());
Map<String, AppIdUserIdMechanism> appIdUserIdMechanisms = applicationContext.getBeansOfType(AppIdUserIdMechanism.class);
if(!appIdUserIdMechanisms.isEmpty()){
vaultClient.setAppIdUserIdMechanism(appIdUserIdMechanisms.values().iterator().next());
}
return vaultClient;
}
@Bean
@@ -40,7 +54,25 @@ public class VaultBootstrapConfiguration {
}
@Bean
public VaultPropertySourceLocator vaultPropertySourceLocator() {
return new VaultPropertySourceLocator(vaultClient(), vaultProperties());
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.cloud.vault", name = "authentication", havingValue = "APPID")
public AppIdUserIdMechanism appIdUserIdMechanism(VaultProperties vaultProperties) {
String userId = vaultProperties.getAppId().getUserId();
Assert.hasText(userId, "UserId (spring.cloud.vault.app-id.user-id) must not be empty.");
switch (userId.toUpperCase()) {
case VaultProperties.AppIdProperties.IP_ADDRESS:
return new IpAddressUserId();
case VaultProperties.AppIdProperties.MAC_ADDRESS:
return new MacAddressUserId(vaultProperties);
default:
return new StaticUserId(vaultProperties);
}
}
}
@Bean
public VaultPropertySourceLocator vaultPropertySourceLocator(ApplicationContext applicationContext) {
return new VaultPropertySourceLocator(vaultClient(applicationContext), vaultProperties());
}
}

View File

@@ -17,42 +17,54 @@
package org.springframework.cloud.vault;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.http.*;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;
/**
* Vault client. This client reads data from Vault secret backends and can authenticate with
* Vault to obtain an access token.
*
* @author Spencer Gibb
* @author Mark Paluch
*/
@RequiredArgsConstructor
public class VaultClient {
public static final String VAULT_TOKEN = "X-Vault-Token";
// protected static final ParameterizedTypeReference<Map<String, String>> typeRef = new ParameterizedTypeReference<Map<String, String>>() { };
public static final String API_VERSION = "v1";
public static final String VAULT_TOKEN = "X-Vault-Token";
@Setter
private RestTemplate rest = new RestTemplate();
@Setter
private AppIdUserIdMechanism appIdUserIdMechanism;
private final VaultProperties properties;
public Map<String, String> read(String key) {
String url = String.format("%s://%s:%s/v1/{backend}/{key}",
this.properties.getScheme(), this.properties.getHost(), this.properties.getPort());
public Map<String, String> read(String key, VaultToken vaultToken) {
HttpHeaders headers = new HttpHeaders();
headers.add(VAULT_TOKEN, this.properties.getToken());
Assert.hasText(key, "Key must not be empty!");
Assert.notNull(vaultToken, "VaultToken must not be null!");
String url = buildUrl();
HttpHeaders headers = createHeaders(vaultToken);
try {
ResponseEntity<VaultResponse> response = this.rest.exchange(url, HttpMethod.GET,
new HttpEntity<>(headers), VaultResponse.class, this.properties.getBackend(), key);
ResponseEntity<VaultResponse> response = this.rest.exchange(url,
HttpMethod.GET, new HttpEntity<>(headers), VaultResponse.class,
this.properties.getBackend(), key);
HttpStatus status = response.getStatusCode();
if (status == HttpStatus.OK) {
@@ -67,4 +79,79 @@ public class VaultClient {
return Collections.emptyMap();
}
}
private HttpHeaders createHeaders(VaultToken vaultToken) {
HttpHeaders headers = new HttpHeaders();
headers.add(VAULT_TOKEN, vaultToken.getToken());
return headers;
}
/**
* Creates a token using a configured authentication mechanism.
*
* @return
*/
public VaultToken createToken() {
if (properties.getAuthentication() == AuthenticationMethod.APPID && appIdUserIdMechanism != null) {
AppIdProperties appId = properties.getAppId();
return createTokenUsingAppId(new AppIdTuple(properties.getApplicationName(), appIdUserIdMechanism.createUserId()), appId);
}
throw new UnsupportedOperationException(String.format(
"Cannot create a token for auth method %s", properties.getAuthentication()));
}
private VaultToken createTokenUsingAppId(AppIdTuple appIdTuple, AppIdProperties appId) {
String url = buildUrl();
Map<String, String> variables = new HashMap<>();
variables.put("backend", "auth/" + appId.getAppIdPath());
variables.put("key", "login");
Map<String, String> login = getAppIdLogin(appIdTuple);
try {
ResponseEntity<VaultResponse> response = this.rest.exchange(url,
HttpMethod.POST, new HttpEntity<>(login), VaultResponse.class,
variables);
HttpStatus status = response.getStatusCode();
if (!status.is2xxSuccessful()) {
throw new IllegalStateException("Cannot login using app-id");
}
VaultResponse body = response.getBody();
String token = (String) body.getAuth().get("client_token");
return VaultToken.of(token, body.getLeaseDuration());
} catch (HttpClientErrorException e) {
if (e.getStatusCode().equals(HttpStatus.BAD_REQUEST)) {
throw new IllegalStateException(String.format(
"Cannot login using app-id: %s", e.getResponseBodyAsString()));
}
throw e;
}
}
private Map<String, String> getAppIdLogin(AppIdTuple appIdTuple) {
Map<String, String> login = new HashMap<>();
login.put("app_id", appIdTuple.getAppId());
login.put("user_id", appIdTuple.getUserId());
return login;
}
private String buildUrl() {
return String.format("%s://%s:%s/%s/{backend}/{key}", this.properties.getScheme(),
this.properties.getHost(), this.properties.getPort(), API_VERSION);
}
@Value
private static class AppIdTuple{
private String appId;
private String userId;
}
}

View File

@@ -24,29 +24,100 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb
* @author Mark Paluch
*/
@ConfigurationProperties("spring.cloud.vault")
@Data
public class VaultProperties {
/**
* Enable Vault config server.
*/
private boolean enabled = true;
/**
* Vault server host.
*/
@NotEmpty
private String host = "127.0.0.1";
/**
* Vault server port.
*/
@Range(min = 1, max = 65535)
private int port = 8200;
/**
* Protocol scheme. Can be either "http" or "https".
*/
private String scheme = "http";
/**
* Name of the default backend.
*/
@NotEmpty
private String backend = "secret";
/**
* Name of the default context.
*/
@NotEmpty
private String defaultContext = "application";
/**
* Profile-separator to combine application name and profile.
*/
@NotEmpty
private String profileSeparator = ",";
@NotEmpty
/**
* Static vault token. Required if {@link #authentication} is {@code TOKEN}.
*/
private String token;
}
private AppIdProperties appId = new AppIdProperties();
/**
* Application name for AppId authentication.
*/
@org.springframework.beans.factory.annotation.Value("${spring.application.name:application}")
private String applicationName;
private AuthenticationMethod authentication = AuthenticationMethod.TOKEN;
@Data
public static class AppIdProperties {
/**
* Property value for UserId generation using a Mac-Address.
* @see MacAddressUserId
*/
public final static String MAC_ADDRESS = "MAC_ADDRESS";
/**
* Property value for UserId generation using an IP-Address.
* @see IpAddressUserId
*/
public final static String IP_ADDRESS = "IP_ADDRESS";
/**
* Mount path of the AppId authentication backend.
*/
private String appIdPath = "app-id";
/**
* Network interface hint for the "MAC_ADDRESS" UserId mechanism.
*/
private String networkInterface = null;
/**
* UserId mechanism. Can be either "MAC_ADDRESS", "IP_ADDRESS". Any other values are passed as UserId.
*/
@NotEmpty
private String userId = MAC_ADDRESS;
}
public enum AuthenticationMethod {
TOKEN, APPID,
}
}

View File

@@ -22,35 +22,73 @@ import java.util.Set;
import lombok.extern.apachecommons.CommonsLog;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.util.Assert;
/**
* @author Spencer Gibb
* @author Mark Paluch
*/
@CommonsLog
public class VaultPropertySource extends EnumerablePropertySource<VaultClient> {
private final VaultProperties vaultProperties;
private String context;
private Map<String, String> properties = new LinkedHashMap<>();
private transient VaultState vaultState;
public VaultPropertySource(String context, VaultClient source) {
public VaultPropertySource(String context, VaultClient source, VaultProperties properties, VaultState state) {
super(context, source);
this.context = context;
this.vaultProperties = properties;
this.vaultState = state;
}
public void init() {
try {
Map<String, String> values = this.source.read(this.context);
Map<String, String> values = this.source.read(this.context, obtainToken());
if (values != null) {
this.properties.putAll(values);
}
} catch (Exception e) {
log.error("Unable to read properties from vault for key "+this.context, e);
log.error("Unable to read properties from vault for key " + this.context, e);
}
}
private VaultToken obtainToken() {
if (vaultState.getToken() != null) {
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.TOKEN) {
Assert.hasText(vaultProperties.getToken(), "Token must not be empty");
vaultState.setToken(VaultToken.of(vaultProperties.getToken()));
return vaultState.getToken();
}
if (vaultProperties.getAuthentication() == AuthenticationMethod.APPID) {
AppIdProperties appId = vaultProperties.getAppId();
Assert.hasText(vaultProperties.getApplicationName(), "AppId must not be empty");
Assert.hasText(appId.getAppIdPath(), "AppIdPath must not be empty");
vaultState.setToken(source.createToken());
return vaultState.getToken();
}
throw new IllegalStateException(
String.format("Authentication method %s not supported", vaultProperties.getAuthentication()));
}
@Override
public Object getProperty(String name) {
return this.properties.get(name);

View File

@@ -35,6 +35,7 @@ public class VaultPropertySourceLocator implements PropertySourceLocator {
private VaultClient vault;
private VaultProperties properties;
private transient final VaultState vaultState = new VaultState();
public VaultPropertySourceLocator(VaultClient vault, VaultProperties properties) {
this.vault = vault;
@@ -74,7 +75,7 @@ public class VaultPropertySourceLocator implements PropertySourceLocator {
}
private VaultPropertySource create(String context) {
return new VaultPropertySource(context, this.vault);
return new VaultPropertySource(context, this.vault, this.properties, this.vaultState);
}
private void addProfiles(List<String> contexts, String baseContext,

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2015 the original author or authors.
* Copyright 2013-2016 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.
@@ -24,11 +24,13 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Spencer Gibb
* @author Mark Paluch
*/
@Data
public class VaultResponse {
private String auth;
private Map<String, Object> auth;
private Map<String, String> data;
private Map<String, String> metadata;
@JsonProperty("lease_duration")
private long leaseDuration;
@JsonProperty("lease_id")

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2016 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.cloud.vault;
import lombok.Data;
/**
* State of the Vault client.
*
* @author Mark Paluch
*/
@Data
class VaultState {
private VaultToken token;
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2016 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.cloud.vault;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.springframework.util.Assert;
/**
* @author Mark Paluch
*/
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class VaultToken {
private String token;
private long leaseDuration;
public static VaultToken of(String token) {
return of(token, 0);
}
public static VaultToken of(String token, long leaseDuration) {
Assert.hasText(token, "Token must not be empty");
return new VaultToken(token, leaseDuration);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2016 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.cloud.vault;
import static org.assertj.core.api.Assertions.*;
import java.util.Collections;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.vault.VaultAppIdCustomMechanismTests.BootstrapConfiguration;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Mark Paluch
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {BootstrapConfiguration.class, VaultAppIdCustomMechanismTests.TestApplication.class})
@IntegrationTest({ "spring.cloud.vault.authentication=appid", "use.custom.config=true", "spring.application.name=VaultAppIdCustomMechanismTests"})
public class VaultAppIdCustomMechanismTests {
@BeforeClass
public static void beforeClass() throws Exception {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
vaultRule.prepare().writeSecret(VaultAppIdCustomMechanismTests.class.getSimpleName(), Collections.singletonMap("vault.value", "foo"));
VaultProperties vaultProperties = Settings.createVaultProperties();
vaultProperties.setAuthentication(VaultProperties.AuthenticationMethod.APPID);
if (!vaultRule.prepare().hasAuth(vaultProperties.getAppId().getAppIdPath())) {
vaultRule.prepare().mountAuth(vaultProperties.getAppId().getAppIdPath());
}
vaultRule.prepare().mapAppId(VaultAppIdCustomMechanismTests.class.getSimpleName());
vaultRule.prepare().mapUserId(VaultAppIdCustomMechanismTests.class.getSimpleName(), new StaticUserIdMechanism().createUserId());
}
@Value("${vault.value}") String configValue;
@Test
public void contextLoads() {
assertThat(configValue).isEqualTo("foo");
}
@SpringBootApplication
public static class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
@Configuration
public static class BootstrapConfiguration {
@Bean
@ConditionalOnProperty("use.custom.config")
AppIdUserIdMechanism appIdUserIdMechanism() {
return new StaticUserIdMechanism();
}
}
public static class StaticUserIdMechanism implements AppIdUserIdMechanism {
@Override
public String createUserId() {
return "static-string";
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2016 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.cloud.vault;
import static org.assertj.core.api.Assertions.*;
import java.util.Collections;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Mark Paluch
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = VaultAppIdTests.TestApplication.class)
@IntegrationTest({"spring.cloud.vault.authentication=appid", "spring.cloud.vault.app-id.user-id=IP_ADDRESS", "spring.application.name=VaultAppIdTests"})
public class VaultAppIdTests {
@BeforeClass
public static void beforeClass() throws Exception {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
vaultRule.prepare().writeSecret(VaultAppIdTests.class.getSimpleName(), Collections.singletonMap("vault.value", "foo"));
VaultProperties vaultProperties = Settings.createVaultProperties();
vaultProperties.setAuthentication(VaultProperties.AuthenticationMethod.APPID);
vaultProperties.getAppId().setUserId(VaultProperties.AppIdProperties.IP_ADDRESS);
if (!vaultRule.prepare().hasAuth(vaultProperties.getAppId().getAppIdPath())) {
vaultRule.prepare().mountAuth(vaultProperties.getAppId().getAppIdPath());
}
vaultRule.prepare().mapAppId(VaultAppIdTests.class.getSimpleName());
vaultRule.prepare().mapUserId(VaultAppIdTests.class.getSimpleName(), new IpAddressUserId().createUserId());
}
@Value("${vault.value}")
String configValue;
@Test
public void contextLoads() {
assertThat(configValue).isEqualTo("foo");
}
@SpringBootApplication
public static class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
}

View File

@@ -1,20 +1,44 @@
package org.springframework.cloud.vault;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.WebIntegrationTest;
import org.springframework.cloud.vault.util.PrepareVault;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = VaultTests.TestApplication.class)
@WebIntegrationTest(randomPort = true)
public class VaultTests {
@BeforeClass
public static void beforeClass() throws Exception {
VaultRule vaultRule = new VaultRule();
vaultRule.before();
vaultRule.prepare().writeSecret("testVaultApp", Collections.singletonMap("vault.value", "foo"));
}
@Value("${vault.value}")
String configValue;
@Test
public void contextLoads() {
assertThat(configValue).isEqualTo("foo");
}
@SpringBootApplication

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2016 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.cloud.vault.integration;
import org.junit.Rule;
import org.springframework.cloud.vault.util.PrepareVault;
import org.springframework.cloud.vault.util.VaultRule;
/**
* Base class for integration tests using Vault.
*
* @author Mark Paluch
*/
public abstract class AbstractIntegrationTests {
@Rule
public final VaultRule vaultRule = new VaultRule();
public final PrepareVault prepare() {
return vaultRule.prepare();
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2016 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.cloud.vault.integration;
import org.junit.Before;
import org.springframework.cloud.vault.IpAddressUserId;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.cloud.vault.util.Settings;
/**
* Integration tests for {@link VaultClient} using {@link AuthenticationMethod#APPID}.
*
* @author Mark Paluch
*/
public class AppIdAuthenticationIntegrationTests extends GenericSecretIntegrationTests {
private VaultClient vaultClient;
@Before
public void setUp() throws Exception {
super.setUp();
VaultProperties vaultProperties = Settings.createVaultProperties();
AppIdProperties appId = configureAppIdProperties();
vaultProperties.setApplicationName("myapp");
vaultProperties.setAuthentication(AuthenticationMethod.APPID);
vaultProperties.setAppId(appId);
if (!prepare().hasAuth(appId.getAppIdPath())) {
prepare().mountAuth(appId.getAppIdPath());
}
IpAddressUserId userIdMechanism = new IpAddressUserId();
String userId = userIdMechanism.createUserId();
prepare().mapAppId(vaultProperties.getApplicationName());
prepare().mapUserId(vaultProperties.getApplicationName(), userId);
vaultClient = new VaultClient(vaultProperties);
vaultClient.setAppIdUserIdMechanism(userIdMechanism);
}
@Override
protected VaultToken createToken() {
return vaultClient.createToken();
}
private AppIdProperties configureAppIdProperties() {
AppIdProperties appId = new AppIdProperties();
appId.setUserId(AppIdProperties.IP_ADDRESS);
return appId;
}
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2016 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.cloud.vault.integration;
import static org.assertj.core.api.Assertions.*;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.cloud.vault.*;
import org.springframework.cloud.vault.VaultProperties.AppIdProperties;
import org.springframework.cloud.vault.VaultProperties.AuthenticationMethod;
import org.springframework.cloud.vault.util.Settings;
/**
* Integration tests for {@link VaultClient} using various UserIds.
*
* @author Mark Paluch
*/
public class AppIdAuthenticationMethodsIntegrationTests extends AbstractIntegrationTests {
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Before
public void setUp() throws Exception {
prepare().mapAppId("myapp");
}
@Test
public void loginUsingIpAddressShouldCreateAToken() throws Exception {
VaultClient vaultClient = new VaultClient(
prepareAppIdAuthenticationMethod(AppIdProperties.IP_ADDRESS, "myapp"));
vaultClient.setAppIdUserIdMechanism(new IpAddressUserId());
assertThat(vaultClient.createToken()).isNotNull();
}
@Test
public void loginUsingStaticUserIdShouldCreateAToken() throws Exception {
VaultProperties vaultProperties = prepareAppIdAuthenticationMethod("my-user-id", "myapp");
VaultClient vaultClient = new VaultClient(vaultProperties);
vaultClient.setAppIdUserIdMechanism(new StaticUserId(vaultProperties));
assertThat(vaultClient.createToken()).isNotNull();
}
@Test
public void loginUsingMacAddressShouldCreateAToken() throws Exception {
VaultProperties vaultProperties = prepareAppIdAuthenticationMethod(AppIdProperties.MAC_ADDRESS, "myapp");
VaultClient vaultClient = new VaultClient(vaultProperties);
vaultClient.setAppIdUserIdMechanism(
new MacAddressUserId(vaultProperties));
assertThat(vaultClient.createToken()).isNotNull();
}
@Test
public void invalidLogin() throws Exception {
expectedException.expect(IllegalStateException.class);
expectedException.expectMessage("Cannot login using app-id");
VaultProperties vaultProperties = prepareAppIdAuthenticationMethod(
AppIdProperties.IP_ADDRESS, "myapp");
vaultProperties.setApplicationName("foobar");
VaultClient vaultClient = new VaultClient(vaultProperties);
vaultClient.setAppIdUserIdMechanism(new MacAddressUserId(vaultProperties));
vaultClient.createToken();
fail("Missing IllegalStateException");
}
private VaultProperties prepareAppIdAuthenticationMethod(String userId, String appId)
throws SocketException {
VaultProperties vaultProperties = Settings.createVaultProperties();
AppIdProperties appIdProperties = new AppIdProperties();
vaultProperties.setApplicationName(appId);
appIdProperties.setUserId(userId);
Enumeration<NetworkInterface> networkInterfaces = NetworkInterface
.getNetworkInterfaces();
NetworkInterface networkInterface = null;
while (networkInterfaces.hasMoreElements()) {
networkInterface = networkInterfaces.nextElement();
if (networkInterface.getHardwareAddress() != null) {
break;
}
}
// make sure we have always a network interface even if the localhost reverse
// lookup maps to an IP address that is not handled by this host.
appIdProperties.setNetworkInterface(networkInterface.getName());
vaultProperties.setAuthentication(AuthenticationMethod.APPID);
vaultProperties.setAppId(appIdProperties);
String userIdValue;
if (userId.equals(AppIdProperties.IP_ADDRESS)) {
userIdValue = new IpAddressUserId().createUserId();
}
else if (userId.equals(AppIdProperties.MAC_ADDRESS)) {
userIdValue = new MacAddressUserId(vaultProperties).createUserId();
}
else {
userIdValue = userId;
}
prepare().mapUserId(vaultProperties.getApplicationName(), userIdValue);
return vaultProperties;
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2016 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.cloud.vault.integration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.web.client.RestTemplate;
/**
* Integration tests for {@link VaultClient} using the generic secret backend.
* @author Mark Paluch
*/
public class GenericSecretIntegrationTests extends AbstractIntegrationTests {
private VaultProperties vaultProperties = Settings.createVaultProperties();
private VaultClient vaultClient = new VaultClient(vaultProperties);
@Before
public void setUp() throws Exception {
prepare().writeSecret("app-name", (Map) createData());
vaultClient.setRest(new RestTemplate());
}
@Test
public void shouldReturnSecretsCorrectly() throws Exception {
Map<String, String> secretProperties = vaultClient.read("app-name",
createToken());
assertThat(secretProperties).containsAllEntriesOf(createExpectedMap());
}
@Test
public void shouldReturnNullIfNotFound() throws Exception {
Map<String, String> secretProperties = vaultClient.read("missing", createToken());
assertThat(secretProperties).isNull();
}
/**
* Can be overridden by subclasses.
*
* @return
*/
protected VaultToken createToken() {
return Settings.token();
}
private Map<String, Object> createData() {
Map<String, Object> data = new HashMap<>();
data.put("string", "value");
data.put("number", "1234");
data.put("boolean", true);
return data;
}
private Map<String, String> createExpectedMap() {
Map<String, String> data = new HashMap<>();
data.put("string", "value");
data.put("number", "1234");
data.put("boolean", "true");
return data;
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2016 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.cloud.vault.integration;
import org.junit.Test;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.cloud.vault.util.PrepareVault;
import org.springframework.cloud.vault.util.Settings;
/**
* Integration tests for {@link PrepareVault}.
*
* @author Mark Paluch
*/
public class PrepareVaultTests {
private VaultProperties vaultProperties = Settings.createVaultProperties();
private PrepareVault prepareVault = new PrepareVault(new TestRestTemplate());
@Test
public void initializeShouldCreateANewVault() throws Exception {
prepareVault.setRootToken(Settings.token());
prepareVault.setVaultProperties(vaultProperties);
if (!prepareVault.isAvailable()) {
VaultToken rootToken = prepareVault.initializeVault();
prepareVault.setRootToken(rootToken);
prepareVault.createToken(vaultProperties.getToken(), "root");
}
}
}

View File

@@ -0,0 +1,434 @@
/*
* Copyright 2016 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.cloud.vault.util;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.cloud.vault.VaultClient;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NonNull;
import lombok.Setter;
import lombok.Value;
/**
* Test helper to prepare various settings within Vault.
*
* @author Mark Paluch
*/
public class PrepareVault {
public final static String INITIALIZE_URL_TEMPLATE = "{baseuri}/sys/init";
public final static String MOUNT_AUTH_URL_TEMPLATE = "{baseuri}/sys/auth/{authBackend}";
public final static String SYS_AUTH_URL_TEMPLATE = "{baseuri}/sys/auth";
public final static String SEAL_STATUS_URL_TEMPLATE = "{baseuri}/sys/seal-status";
public final static String UNSEAL_URL_TEMPLATE = "{baseuri}/sys/unseal";
public final static String CREATE_TOKEN_URL_TEMPLATE = "{baseuri}/auth/token/create-orphan";
public final static String WRITE_URL_TEMPLATE = "{baseuri}/{path}";
public static final ParameterizedTypeReference<Map<String, Map<String, String>>> MAP_OF_MAPS_TYPE = new ParameterizedTypeReference<Map<String, Map<String, String>>>() {
};
private final RestTemplate restTemplate;
@Setter
@NonNull
private VaultProperties vaultProperties;
@Setter
@NonNull
private VaultToken rootToken;
public PrepareVault(RestTemplate restTemplate) {
Assert.notNull(restTemplate, "RestTemplate must not be null");
this.restTemplate = restTemplate;
}
/**
* Initialize Vault and unseal the vault.
*
* @return the root token.
*/
public VaultToken initializeVault() {
Assert.notNull(vaultProperties, "VaultProperties must not be null");
Map<String, String> parameters = parameters(vaultProperties);
int createKeys = 2;
int requiredKeys = 2;
InitializeVault initializeVault = InitializeVault.of(createKeys, requiredKeys);
ResponseEntity<VaultInitialized> initResponse = restTemplate.exchange(
INITIALIZE_URL_TEMPLATE, HttpMethod.PUT,
new HttpEntity<>(initializeVault), VaultInitialized.class, parameters);
if (!initResponse.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot initialize vault: " + initResponse.toString());
}
VaultInitialized initialized = initResponse.getBody();
for (int i = 0; i < requiredKeys; i++) {
UnsealKey unsealKey = UnsealKey.of(initialized.getKeys().get(i));
ResponseEntity<UnsealProgress> unsealResponse = restTemplate.exchange(
UNSEAL_URL_TEMPLATE, HttpMethod.PUT, new HttpEntity<>(unsealKey),
UnsealProgress.class, parameters);
UnsealProgress unsealProgress = unsealResponse.getBody();
if (!unsealProgress.isSealed()) {
break;
}
}
return VaultToken.of(initialized.getRootToken());
}
/**
* Create a token for the given {@code tokenId} and {@code policy}.
*
* @param tokenId
* @param policy
* @return
*/
public VaultToken createToken(String tokenId, String policy) {
Map<String, String> parameters = parameters(vaultProperties);
CreateToken createToken = new CreateToken();
createToken.setId(tokenId);
if (policy != null) {
createToken.setPolicies(Collections.singletonList(policy));
}
HttpHeaders headers = authenticatedHeaders();
HttpEntity<CreateToken> entity = new HttpEntity<>(createToken, headers);
ResponseEntity<TokenCreated> createTokenResponse = restTemplate.exchange(
CREATE_TOKEN_URL_TEMPLATE, HttpMethod.POST, entity, TokenCreated.class,
parameters);
if (!createTokenResponse.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot create token: " + createTokenResponse.toString());
}
AuthToken authToken = createTokenResponse.getBody().getAuth();
return VaultToken.of(authToken.getClientToken());
}
/**
* Check whether Vault is available (vault created and unsealed).
*
* @return
*/
public boolean isAvailable() {
Map<String, String> parameters = parameters(vaultProperties);
ResponseEntity<String> exchange = restTemplate
.getForEntity(SEAL_STATUS_URL_TEMPLATE, String.class, parameters);
if (exchange.getStatusCode().is2xxSuccessful()) {
return true;
}
if (exchange.getStatusCode().is4xxClientError()) {
return false;
}
throw new IllegalStateException("Vault error: " + exchange.toString());
}
/**
* Mount an auth backend.
*
* @param authBackend
*/
public void mountAuth(String authBackend) {
Assert.hasText(authBackend, "AuthBackend must not be empty");
Map<String, String> parameters = parameters(vaultProperties);
parameters.put("authBackend", authBackend);
Map<String, String> requestEntity = Collections.singletonMap("type", authBackend);
HttpEntity<Map<String, String>> entity = new HttpEntity<>(requestEntity,
authenticatedHeaders());
ResponseEntity<String> responseEntity = restTemplate.exchange(
MOUNT_AUTH_URL_TEMPLATE, HttpMethod.POST, entity, String.class,
parameters);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot create mount auth backend: " + responseEntity.toString());
}
responseEntity.getBody();
}
/**
* Check whether a auth-backend is enabled.
*
* @param authBackend
* @return
*/
public boolean hasAuth(String authBackend) {
Assert.hasText(authBackend, "AuthBackend must not be empty");
Map<String, String> parameters = parameters(vaultProperties);
parameters.put("authBackend", authBackend);
HttpEntity<Map<String, String>> entity = new HttpEntity<>(authenticatedHeaders());
ResponseEntity<Map<String, Map<String, String>>> responseEntity = restTemplate
.exchange(SYS_AUTH_URL_TEMPLATE, HttpMethod.GET, entity, MAP_OF_MAPS_TYPE,
parameters);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
"Cannot create mount auth backend: " + responseEntity.toString());
}
Map<String, Map<String, String>> body = responseEntity.getBody();
for (Entry<String, Map<String, String>> entry : body.entrySet()) {
if (entry.getKey().contains(authBackend)
&& authBackend.equals(entry.getValue().get("type"))) {
return true;
}
}
return false;
}
/**
* Write key-value data to the Vault secret backend.
*
* @param path
* @param data
*/
public void writeSecret(String path, Map<String, ?> data) {
Assert.hasText(path, "Path must not be empty");
write("secret/" + path, data);
}
/**
* Write key-value data to a path in Vault.
*
* @param path
* @param data
*/
public void write(String path, Map<String, ?> data) {
Assert.hasText(path, "Path must not be empty");
Assert.notNull(data, "Data must not be null");
HttpHeaders headers = authenticatedHeaders();
Map<String, String> parameters = parameters(vaultProperties);
parameters.put("path", path);
ResponseEntity<String> exchange = restTemplate.exchange(WRITE_URL_TEMPLATE,
HttpMethod.PUT, new HttpEntity<Object>(data, headers), String.class,
parameters);
if (!exchange.getStatusCode().is2xxSuccessful()) {
throw new IllegalStateException(
String.format("Cannot write to %s: %s", path, exchange.getBody()));
}
}
/**
* Create an userId to appId mapping.
*
* @param appId
* @param userId
*/
public void mapUserId(String appId, String userId) {
Map<String, String> userIdData = new HashMap<>();
userIdData.put("value", appId); // name of the app-id
userIdData.put("cidr_block", "0.0.0.0/0");
String appIdPath = vaultProperties.getAppId().getAppIdPath();
if (!hasAuth(appIdPath)) {
mountAuth(appIdPath);
}
write(String.format("auth/%s/map/user-id/%s", appIdPath, userId), userIdData);
}
/**
* Create an appId mapping.
*
* @param appId
*/
public void mapAppId(String appId) {
Map<String, String> appIdData = new HashMap<>();
appIdData.put("value", "root"); // policy
appIdData.put("display_name", "this is my test application");
write(String.format("auth/%s/map/app-id/%s",
vaultProperties.getAppId().getAppIdPath(), appId), appIdData);
}
private HttpHeaders authenticatedHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.add(VaultClient.VAULT_TOKEN, rootToken.getToken());
return headers;
}
private Map<String, String> parameters(VaultProperties vaultProperties) {
Map<String, String> parameters = new HashMap<>();
String baseUri = String.format("%s://%s:%s/%s", vaultProperties.getScheme(),
vaultProperties.getHost(), vaultProperties.getPort(),
VaultClient.API_VERSION);
parameters.put("baseuri", baseUri);
return parameters;
}
/**
* @author Mark Paluch
*/
@Data
static class TokenCreated {
@JsonProperty("lease_duration")
private long leaseDuration;
@JsonProperty("renewable")
private boolean renewable;
@JsonProperty("auth")
private AuthToken auth;
}
/**
* @author Mark Paluch
*/
@Value(staticConstructor = "of")
static class InitializeVault {
@JsonProperty("secret_shares")
private int secretShares;
@JsonProperty("secret_threshold")
private int secretThreshold;
}
/**
* @author Mark Paluch
*/
@Data
static class CreateToken {
@JsonProperty("id")
private String id;
@JsonProperty("policies")
private List<String> policies;
@JsonProperty("ttl")
private String ttl;
}
/**
* @author Mark Paluch
*/
@Value(staticConstructor = "of")
static class UnsealKey {
@JsonProperty
@NonNull
private String key;
}
/**
* @author Mark Paluch
*/
@Data
static class UnsealProgress {
@JsonProperty("sealed")
private boolean sealed;
@JsonProperty("t")
private int t;
@JsonProperty("n")
private int n;
@JsonProperty("progress")
private int progress;
}
/**
* @author Mark Paluch
*/
@Data
static class VaultInitialized {
@JsonProperty("keys")
private List<String> keys;
@JsonProperty("root_token")
private String rootToken;
}
/**
* @author Mark Paluch
*/
@Data
public static class AuthToken {
@JsonProperty("client_token")
private String clientToken;
@JsonProperty("policies")
private List<String> policies;
@JsonProperty("metadata")
private Map<String, Object> metadata;
@JsonProperty("lease_duration")
private long leaseDuration;
@JsonProperty("renewable")
private boolean renewable;
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2016 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.cloud.vault.util;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
/**
* Utility to retrieve settings during test.
*
* @author Mark Paluch
*/
public class Settings {
/**
*
* @return the vault properties.
*/
public static VaultProperties createVaultProperties() {
VaultProperties vaultProperties = new VaultProperties();
vaultProperties.setToken(token().getToken());
vaultProperties.setHost(System.getProperty("vault.host", "localhost"));
return vaultProperties;
}
/**
* @return the token to use during tests.
*/
public static VaultToken token() {
return VaultToken.of(System.getProperty("vault.token",
"00000000-0000-0000-0000-000000000000"));
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2016 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.cloud.vault.util;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import org.junit.rules.ExternalResource;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.cloud.vault.VaultProperties;
import org.springframework.cloud.vault.VaultToken;
/**
* Vault rule to ensure a running and prepared Vault.
*
* @author Mark Paluch
*/
public class VaultRule extends ExternalResource {
private final VaultProperties vaultProperties;
private final PrepareVault prepareVault = new PrepareVault(new TestRestTemplate());
public VaultRule() {
this(Settings.createVaultProperties());
}
public VaultRule(VaultProperties vaultProperties) {
this.vaultProperties = vaultProperties;
}
@Override
public void before() {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(InetAddress.getByName("localhost"),
vaultProperties.getPort()));
socket.close();
}
catch (Exception ex) {
throw new IllegalStateException(String.format(
"Vault is not running on localhost:%d which is required to run a test using @Rule %s",
vaultProperties.getPort(), getClass().getSimpleName()));
}
prepareVault.setVaultProperties(vaultProperties);
if (!prepareVault.isAvailable()) {
VaultToken rootToken = prepareVault.initializeVault();
prepareVault.setRootToken(rootToken);
prepareVault.createToken(vaultProperties.getToken(), "root");
}
else {
prepareVault.setRootToken(Settings.token());
}
}
public PrepareVault prepare() {
return prepareVault;
}
}

View File

@@ -0,0 +1,3 @@
# Bootstrap Configuration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.vault.VaultAppIdCustomMechanismTests.BootstrapConfiguration

View File

@@ -0,0 +1,7 @@
backend "inmem" {
}
listener "tcp" {
address = "127.0.0.1:8200"
tls_disable = 1
}