diff --git a/pom.xml b/pom.xml index 157aa223..bfd59fdd 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,12 @@ spring-boot-starter-test test + + org.assertj + assertj-core + 3.3.0 + test + diff --git a/src/main/java/org/springframework/cloud/vault/AppIdUserIdMechanism.java b/src/main/java/org/springframework/cloud/vault/AppIdUserIdMechanism.java new file mode 100644 index 00000000..ac3fd2f7 --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/AppIdUserIdMechanism.java @@ -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(); + +} diff --git a/src/main/java/org/springframework/cloud/vault/IpAddressUserId.java b/src/main/java/org/springframework/cloud/vault/IpAddressUserId.java new file mode 100644 index 00000000..5b30edda --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/IpAddressUserId.java @@ -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); + } + } +} diff --git a/src/main/java/org/springframework/cloud/vault/MacAddressUserId.java b/src/main/java/org/springframework/cloud/vault/MacAddressUserId.java new file mode 100644 index 00000000..238d052d --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/MacAddressUserId.java @@ -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 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 interfaces) { + + if (interfaces.size() > hint.intValue() && hint.intValue() >= 0) { + return interfaces.get(hint.intValue()); + } + + return null; + } + + private NetworkInterface getNetworkInterface(String hint, List interfaces) { + + for (NetworkInterface anInterface : interfaces) { + if (hint.equals(anInterface.getDisplayName()) || hint.equals(anInterface.getName())) { + return anInterface; + } + } + + return null; + } +} diff --git a/src/main/java/org/springframework/cloud/vault/Sha256.java b/src/main/java/org/springframework/cloud/vault/Sha256.java new file mode 100644 index 00000000..1dcc6758 --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/Sha256.java @@ -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 algorithm 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 + "]"); + } + } +} diff --git a/src/main/java/org/springframework/cloud/vault/StaticUserId.java b/src/main/java/org/springframework/cloud/vault/StaticUserId.java new file mode 100644 index 00000000..8b375252 --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/StaticUserId.java @@ -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(); + } +} diff --git a/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java b/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java index 1749087e..da21f59c 100644 --- a/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java +++ b/src/main/java/org/springframework/cloud/vault/VaultBootstrapConfiguration.java @@ -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 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); + } } -} \ No newline at end of file + + @Bean + public VaultPropertySourceLocator vaultPropertySourceLocator(ApplicationContext applicationContext) { + return new VaultPropertySourceLocator(vaultClient(applicationContext), vaultProperties()); + } +} diff --git a/src/main/java/org/springframework/cloud/vault/VaultClient.java b/src/main/java/org/springframework/cloud/vault/VaultClient.java index 04159330..f0047220 100644 --- a/src/main/java/org/springframework/cloud/vault/VaultClient.java +++ b/src/main/java/org/springframework/cloud/vault/VaultClient.java @@ -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> typeRef = new ParameterizedTypeReference>() { }; + 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 read(String key) { - String url = String.format("%s://%s:%s/v1/{backend}/{key}", - this.properties.getScheme(), this.properties.getHost(), this.properties.getPort()); + public Map 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 response = this.rest.exchange(url, HttpMethod.GET, - new HttpEntity<>(headers), VaultResponse.class, this.properties.getBackend(), key); + ResponseEntity 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(); } -} \ No newline at end of file + + 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 variables = new HashMap<>(); + variables.put("backend", "auth/" + appId.getAppIdPath()); + variables.put("key", "login"); + + Map login = getAppIdLogin(appIdTuple); + + try { + ResponseEntity 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 getAppIdLogin(AppIdTuple appIdTuple) { + + Map 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; + } +} diff --git a/src/main/java/org/springframework/cloud/vault/VaultProperties.java b/src/main/java/org/springframework/cloud/vault/VaultProperties.java index 5c215d39..f6f081e3 100644 --- a/src/main/java/org/springframework/cloud/vault/VaultProperties.java +++ b/src/main/java/org/springframework/cloud/vault/VaultProperties.java @@ -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; -} \ No newline at end of file + + 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, + } +} diff --git a/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java b/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java index a2f4e298..4777073d 100644 --- a/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java +++ b/src/main/java/org/springframework/cloud/vault/VaultPropertySource.java @@ -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 { + private final VaultProperties vaultProperties; + private String context; - private Map 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 values = this.source.read(this.context); + Map 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); diff --git a/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java b/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java index 3273d491..d51de11f 100644 --- a/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java +++ b/src/main/java/org/springframework/cloud/vault/VaultPropertySourceLocator.java @@ -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 contexts, String baseContext, diff --git a/src/main/java/org/springframework/cloud/vault/VaultResponse.java b/src/main/java/org/springframework/cloud/vault/VaultResponse.java index de33f617..e50b4362 100644 --- a/src/main/java/org/springframework/cloud/vault/VaultResponse.java +++ b/src/main/java/org/springframework/cloud/vault/VaultResponse.java @@ -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 auth; private Map data; + private Map metadata; @JsonProperty("lease_duration") private long leaseDuration; @JsonProperty("lease_id") diff --git a/src/main/java/org/springframework/cloud/vault/VaultState.java b/src/main/java/org/springframework/cloud/vault/VaultState.java new file mode 100644 index 00000000..96207534 --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/VaultState.java @@ -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; +} diff --git a/src/main/java/org/springframework/cloud/vault/VaultToken.java b/src/main/java/org/springframework/cloud/vault/VaultToken.java new file mode 100644 index 00000000..292048e9 --- /dev/null +++ b/src/main/java/org/springframework/cloud/vault/VaultToken.java @@ -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); + } +} diff --git a/src/test/java/org/springframework/cloud/vault/VaultAppIdCustomMechanismTests.java b/src/test/java/org/springframework/cloud/vault/VaultAppIdCustomMechanismTests.java new file mode 100644 index 00000000..9eefeb3c --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/VaultAppIdCustomMechanismTests.java @@ -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"; + } + } +} diff --git a/src/test/java/org/springframework/cloud/vault/VaultAppIdTests.java b/src/test/java/org/springframework/cloud/vault/VaultAppIdTests.java new file mode 100644 index 00000000..af052a27 --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/VaultAppIdTests.java @@ -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); + } + } +} diff --git a/src/test/java/org/springframework/cloud/vault/VaultTests.java b/src/test/java/org/springframework/cloud/vault/VaultTests.java index 58a1513e..bd67192c 100644 --- a/src/test/java/org/springframework/cloud/vault/VaultTests.java +++ b/src/test/java/org/springframework/cloud/vault/VaultTests.java @@ -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 diff --git a/src/test/java/org/springframework/cloud/vault/integration/AbstractIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/AbstractIntegrationTests.java new file mode 100644 index 00000000..207d95d7 --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/integration/AbstractIntegrationTests.java @@ -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(); + } +} diff --git a/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationIntegrationTests.java new file mode 100644 index 00000000..78c2ed20 --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationIntegrationTests.java @@ -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; + } + +} diff --git a/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationMethodsIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationMethodsIntegrationTests.java new file mode 100644 index 00000000..3cc0b5bb --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/integration/AppIdAuthenticationMethodsIntegrationTests.java @@ -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 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; + } +} diff --git a/src/test/java/org/springframework/cloud/vault/integration/GenericSecretIntegrationTests.java b/src/test/java/org/springframework/cloud/vault/integration/GenericSecretIntegrationTests.java new file mode 100644 index 00000000..373f8dc9 --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/integration/GenericSecretIntegrationTests.java @@ -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 secretProperties = vaultClient.read("app-name", + createToken()); + + assertThat(secretProperties).containsAllEntriesOf(createExpectedMap()); + } + + @Test + public void shouldReturnNullIfNotFound() throws Exception { + + Map secretProperties = vaultClient.read("missing", createToken()); + + assertThat(secretProperties).isNull(); + } + + /** + * Can be overridden by subclasses. + * + * @return + */ + protected VaultToken createToken() { + return Settings.token(); + } + + private Map createData() { + Map data = new HashMap<>(); + data.put("string", "value"); + data.put("number", "1234"); + data.put("boolean", true); + return data; + } + + private Map createExpectedMap() { + Map data = new HashMap<>(); + data.put("string", "value"); + data.put("number", "1234"); + data.put("boolean", "true"); + return data; + } + +} diff --git a/src/test/java/org/springframework/cloud/vault/integration/PrepareVaultTests.java b/src/test/java/org/springframework/cloud/vault/integration/PrepareVaultTests.java new file mode 100644 index 00000000..01f96ce6 --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/integration/PrepareVaultTests.java @@ -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"); + } + } +} diff --git a/src/test/java/org/springframework/cloud/vault/util/PrepareVault.java b/src/test/java/org/springframework/cloud/vault/util/PrepareVault.java new file mode 100644 index 00000000..b52a622b --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/util/PrepareVault.java @@ -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_OF_MAPS_TYPE = new ParameterizedTypeReference>>() { + + }; + + 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 parameters = parameters(vaultProperties); + + int createKeys = 2; + int requiredKeys = 2; + + InitializeVault initializeVault = InitializeVault.of(createKeys, requiredKeys); + + ResponseEntity 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 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 parameters = parameters(vaultProperties); + + CreateToken createToken = new CreateToken(); + createToken.setId(tokenId); + if (policy != null) { + createToken.setPolicies(Collections.singletonList(policy)); + } + + HttpHeaders headers = authenticatedHeaders(); + + HttpEntity entity = new HttpEntity<>(createToken, headers); + + ResponseEntity 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 parameters = parameters(vaultProperties); + + ResponseEntity 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 parameters = parameters(vaultProperties); + parameters.put("authBackend", authBackend); + + Map requestEntity = Collections.singletonMap("type", authBackend); + + HttpEntity> entity = new HttpEntity<>(requestEntity, + authenticatedHeaders()); + + ResponseEntity 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 parameters = parameters(vaultProperties); + parameters.put("authBackend", authBackend); + + HttpEntity> entity = new HttpEntity<>(authenticatedHeaders()); + + ResponseEntity>> 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> body = responseEntity.getBody(); + for (Entry> 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 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 data) { + + Assert.hasText(path, "Path must not be empty"); + Assert.notNull(data, "Data must not be null"); + + HttpHeaders headers = authenticatedHeaders(); + + Map parameters = parameters(vaultProperties); + parameters.put("path", path); + + ResponseEntity exchange = restTemplate.exchange(WRITE_URL_TEMPLATE, + HttpMethod.PUT, new HttpEntity(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 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 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 parameters(VaultProperties vaultProperties) { + + Map 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 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 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 policies; + + @JsonProperty("metadata") + private Map metadata; + + @JsonProperty("lease_duration") + private long leaseDuration; + + @JsonProperty("renewable") + private boolean renewable; + } +} diff --git a/src/test/java/org/springframework/cloud/vault/util/Settings.java b/src/test/java/org/springframework/cloud/vault/util/Settings.java new file mode 100644 index 00000000..f62db9e9 --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/util/Settings.java @@ -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")); + } +} diff --git a/src/test/java/org/springframework/cloud/vault/util/VaultRule.java b/src/test/java/org/springframework/cloud/vault/util/VaultRule.java new file mode 100644 index 00000000..3c66a92f --- /dev/null +++ b/src/test/java/org/springframework/cloud/vault/util/VaultRule.java @@ -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; + } + +} diff --git a/src/test/resources/META-INF/spring.factories b/src/test/resources/META-INF/spring.factories new file mode 100644 index 00000000..852326b6 --- /dev/null +++ b/src/test/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Bootstrap Configuration +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.vault.VaultAppIdCustomMechanismTests.BootstrapConfiguration diff --git a/src/test/resources/vault.conf b/src/test/resources/vault.conf new file mode 100644 index 00000000..33be49bc --- /dev/null +++ b/src/test/resources/vault.conf @@ -0,0 +1,7 @@ +backend "inmem" { +} + +listener "tcp" { + address = "127.0.0.1:8200" + tls_disable = 1 +} \ No newline at end of file