diff --git a/.travis.yml b/.travis.yml index 44c5a261..2f6cb24c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: java jdk: - oraclejdk8 -sudo: false +sudo: required addons: apt: @@ -33,9 +33,10 @@ before_install: - test ! -f ~/.m2/settings.xml || rm ~/.m2/settings.xml install: + - src/test/bash/minikube_ci_initialize.sh - src/test/bash/start.sh -script: ./mvnw clean verify -P${PROFILE:-ci} +script: ./mvnw clean verify -DMINIKUBE_IP=$(./minikube ip) -P${PROFILE:-ci} after_script: - pkill vault diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeAuthentication.java new file mode 100644 index 00000000..b21859e1 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeAuthentication.java @@ -0,0 +1,106 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestOperations; + +/** + * Kubernetes implementation of {@link ClientAuthentication}. {@link KubeAuthentication} + * uses a Kubernetes Service Account JSON Web Token to login into Vault. JWT and Role are + * sent in the login request to Vault to obtain a {@link VaultToken}. + * + * @author Michal Budzyn + * @see KubeAuthenticationOptions + * @see RestOperations + * @see Auth Backend: + * Kubernetes + */ +public class KubeAuthentication implements ClientAuthentication { + + private static final Log logger = LogFactory.getLog(KubeAuthentication.class); + + private final KubeAuthenticationOptions options; + + private final RestOperations restOperations; + + /** + * Create a {@link KubeAuthentication} using {@link KubeAuthenticationOptions} and + * {@link RestOperations}. + * + * @param options must not be {@literal null}. + * @param restOperations must not be {@literal null}. + */ + public KubeAuthentication(KubeAuthenticationOptions options, + RestOperations restOperations) { + + Assert.notNull(options, "KubeAuthenticationOptions must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.options = options; + this.restOperations = restOperations; + } + + private static Map getKubeLogin(String role, String jwt) { + + Assert.hasText(role, "role must not be empty"); + Assert.hasText(role, "jwt must not be empty"); + + Map login = new HashMap<>(); + + login.put("jwt", jwt); + login.put("role", role); + + return login; + } + + @Override + public VaultToken login() throws VaultException { + return createTokenUsingKubernetes(); + } + + private VaultToken createTokenUsingKubernetes() { + + Map login = getKubeLogin(options.getRole(), + options.getJwtSupplier().getKubeJwt()); + + try { + VaultResponse response = restOperations.postForObject("auth/{mount}/login", + login, VaultResponse.class, options.getPath()); + + Assert.state(response != null && response.getAuth() != null, + "Auth field must not be null"); + + logger.debug("Login successful using Kubernetes authentication"); + + return LoginTokenUtil.from(response.getAuth()); + } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format("Cannot login using kubernetes: %s", + VaultResponses.getError(e.getResponseBodyAsString()))); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeAuthenticationOptions.java new file mode 100644 index 00000000..03954e0f --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeAuthenticationOptions.java @@ -0,0 +1,118 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import org.springframework.util.Assert; + +/** + * Authentication options for {@link KubeAuthentication}. + *

+ * Authentication options provide the path, role and jwt supplier. + * {@link KubeAuthentication} can be constructed using {@link #builder()}. Instances of + * this class are immutable once constructed. + * + * @author Michal Budzyn + * @see KubeAuthentication + * @see #builder() + */ +public class KubeAuthenticationOptions { + + public static final String DEFAULT_KUBERNETES_AUTHENTICATION_PATH = "kubernetes"; + + /** + * Path of the kubernetes authentication backend mount. + */ + private final String path; + + /** + * The Role. + */ + private final String role; + + /** + * {@link KubeJwtSupplier} instance to obtain a service account JSON Web Tokens. + */ + private final KubeJwtSupplier jwtSupplier; + + private KubeAuthenticationOptions(String path, String role, + KubeJwtSupplier jwtSupplier) { + + this.path = path; + this.role = role; + this.jwtSupplier = jwtSupplier; + } + + public static KubernetesAuthenticationOptionsBuilder builder() { + return new KubernetesAuthenticationOptionsBuilder(); + } + + public String getPath() { + return path; + } + + public String getRole() { + return role; + } + + public KubeJwtSupplier getJwtSupplier() { + return jwtSupplier; + } + + /** + * Builder for {@link KubeAuthenticationOptions}. + */ + public static class KubernetesAuthenticationOptionsBuilder { + private String path = DEFAULT_KUBERNETES_AUTHENTICATION_PATH; + + private String role; + + private KubeJwtSupplier jwtSupplier; + + public KubernetesAuthenticationOptionsBuilder path(String path) { + + Assert.hasText(path, "Path must not be empty"); + + this.path = path; + return this; + } + + public KubernetesAuthenticationOptionsBuilder role(String role) { + + Assert.hasText(role, "Role must not be empty"); + + this.role = role; + return this; + } + + public KubernetesAuthenticationOptionsBuilder jwtSupplier( + KubeJwtSupplier jwtSupplier) { + + Assert.notNull(jwtSupplier, "JwtSupplier must not be null"); + + this.jwtSupplier = jwtSupplier; + return this; + } + + public KubeAuthenticationOptions build() { + + Assert.notNull(role, "Role must not be null"); + Assert.notNull(path, "Path must not be null"); + Assert.notNull(jwtSupplier, "JwtSupplier must not be null"); + + return new KubeAuthenticationOptions(path, role, jwtSupplier); + } + } +} \ No newline at end of file diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeJwtSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeJwtSupplier.java new file mode 100644 index 00000000..10fe8376 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeJwtSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +/** + * Interface to obtain a Kubernetes Service Account Token for Kubernetes authentication. + * Implementations are used by {@link KubeAuthentication}. + * + * @author Michal Budzyn + * @see KubeAuthentication + */ +public interface KubeJwtSupplier { + + /** + * Get a JWT for Kubernetes authentication. + * + * @return the Kubernetes Service Account JWT. + */ + String getKubeJwt(); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeServiceAccountTokenFile.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeServiceAccountTokenFile.java new file mode 100644 index 00000000..00359feb --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubeServiceAccountTokenFile.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.StreamUtils; +import org.springframework.vault.VaultException; + +/** + * Mechanism to retrieve a Kubernetes service account token. + *

+ * A file containing a token for a pod’s service account is automatically mounted at + * /var/run/secrets/kubernetes.io/serviceaccount/token + * + * @author Michal Budzyn + * @see KubeJwtSupplier + */ +public class KubeServiceAccountTokenFile implements KubeJwtSupplier { + + public static final String DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + + private final Resource resource; + + public KubeServiceAccountTokenFile() { + this(DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE); + } + + public KubeServiceAccountTokenFile(String fileName) { + this(new FileSystemResource(fileName)); + } + + public KubeServiceAccountTokenFile(File file) { + this(new FileSystemResource(file)); + } + + public KubeServiceAccountTokenFile(Resource resource) { + this.resource = resource; + } + + @Override + public String getKubeJwt() { + try (InputStream is = resource.getInputStream()) { + return StreamUtils.copyToString(is, US_ASCII); + } + catch (IOException e) { + throw new VaultException( + String.format("Kube JWT token retrieval from %s failed", resource), + e); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/config/EnvironmentVaultConfiguration.java b/spring-vault-core/src/main/java/org/springframework/vault/config/EnvironmentVaultConfiguration.java index 6f475c34..6f49d65a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/config/EnvironmentVaultConfiguration.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/config/EnvironmentVaultConfiguration.java @@ -32,19 +32,23 @@ import org.springframework.vault.authentication.AppRoleAuthentication; import org.springframework.vault.authentication.AppRoleAuthenticationOptions; import org.springframework.vault.authentication.AwsEc2Authentication; import org.springframework.vault.authentication.AwsEc2AuthenticationOptions; +import org.springframework.vault.authentication.AwsEc2AuthenticationOptions.AwsEc2AuthenticationOptionsBuilder; import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.ClientCertificateAuthentication; import org.springframework.vault.authentication.CubbyholeAuthentication; import org.springframework.vault.authentication.CubbyholeAuthenticationOptions; import org.springframework.vault.authentication.IpAddressUserId; +import org.springframework.vault.authentication.KubeAuthentication; +import org.springframework.vault.authentication.KubeAuthenticationOptions; +import org.springframework.vault.authentication.KubeJwtSupplier; +import org.springframework.vault.authentication.KubeServiceAccountTokenFile; import org.springframework.vault.authentication.MacAddressUserId; import org.springframework.vault.authentication.StaticUserId; import org.springframework.vault.authentication.TokenAuthentication; -import org.springframework.vault.authentication.AwsEc2AuthenticationOptions.AwsEc2AuthenticationOptionsBuilder; import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.support.SslConfiguration; -import org.springframework.vault.support.VaultToken; import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; +import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestOperations; /** @@ -125,6 +129,7 @@ import org.springframework.web.client.RestOperations; * * * @author Mark Paluch + * @author Michal Budzyn * @see org.springframework.core.env.Environment * @see org.springframework.core.env.PropertySource * @see VaultEndpoint @@ -133,6 +138,7 @@ import org.springframework.web.client.RestOperations; * @see AwsEc2Authentication * @see ClientCertificateAuthentication * @see CubbyholeAuthentication + * @see KubeAuthentication */ @Configuration public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration implements @@ -224,7 +230,8 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration im return new ClientCertificateAuthentication(restOperations()); case CUBBYHOLE: return cubbyholeAuthentication(); - + case KUBERNETES: + return kubeAuthentication(); default: throw new IllegalStateException(String.format( "Vault authentication method %s is not supported with %s", @@ -325,6 +332,23 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration im return new CubbyholeAuthentication(options, restOperations()); } + protected ClientAuthentication kubeAuthentication() { + + String role = getProperty("vault.kubernetes.role"); + Assert.hasText(role, "Vault Kubernetes authentication: role must not be empty"); + + String tokenFile = getProperty("vault.kubernetes.service-account-token-file"); + if (!StringUtils.hasText(tokenFile)) { + tokenFile = KubeServiceAccountTokenFile.DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE; + } + KubeJwtSupplier jwtSupplier = new KubeServiceAccountTokenFile(tokenFile); + + KubeAuthenticationOptions authenticationOptions = KubeAuthenticationOptions + .builder().role(role).jwtSupplier(jwtSupplier).build(); + + return new KubeAuthentication(authenticationOptions, restOperations()); + } + @Nullable private String getProperty(String key) { return getEnvironment().getProperty(key); @@ -342,6 +366,6 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration im } enum AuthenticationMethod { - TOKEN, APPID, APPROLE, AWS_EC2, CERT, CUBBYHOLE; + TOKEN, APPID, APPROLE, AWS_EC2, CERT, CUBBYHOLE, KUBERNETES; } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..042695db --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationIntegrationTestBase.java @@ -0,0 +1,79 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import static org.junit.Assume.assumeTrue; +import static org.springframework.vault.util.Settings.findWorkDir; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.assertj.core.util.Files; +import org.junit.Before; +import org.springframework.util.StringUtils; +import org.springframework.vault.core.RestOperationsCallback; +import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.Version; + +/** + * Integration test base class for {@link KubeAuthentication} tests. + * + * @author Michal Budzyn + */ +public abstract class KubeAuthenticationIntegrationTestBase + extends IntegrationTestSupport { + + @Before + public void before() { + + String minikubeIp = System.getProperty("MINIKUBE_IP"); + assumeTrue(StringUtils.hasText(minikubeIp) + && prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("0.8.3"))); + + if (!prepare().hasAuth("kubernetes")) { + prepare().mountAuth("kubernetes"); + } + + prepare().getVaultOperations() + .doWithSession((RestOperationsCallback) restOperations -> { + File workDir = findWorkDir(); + + String certificate = Files.contentOf( + new File(workDir, "minikube/ca.crt"), + StandardCharsets.US_ASCII); + + String host = String.format("https://%s:8443", minikubeIp); + + Map kubeConfig = new HashMap<>(); + kubeConfig.put("kubernetes_ca_cert", certificate); + kubeConfig.put("kubernetes_host", host); + restOperations.postForEntity("auth/kubernetes/config", kubeConfig, + Map.class); + + Map roleData = new HashMap<>(); + roleData.put("bound_service_account_names", "default"); + roleData.put("bound_service_account_namespaces", "default"); + roleData.put("policies", "default"); + roleData.put("ttl", "1h"); + + return restOperations.postForEntity("auth/kubernetes/role/my-role", + roleData, Map.class); + + }); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationIntegrationTests.java new file mode 100644 index 00000000..d4de8d82 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.vault.util.Settings.findWorkDir; + +import java.io.File; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.vault.VaultException; +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; +import org.springframework.web.client.RestTemplate; + +/** + * Integration tests for {@link KubeAuthentication}. + * + * @author Michal Budzyn + */ +public class KubeAuthenticationIntegrationTests + extends KubeAuthenticationIntegrationTestBase { + + @Test + public void shouldLoginSuccessfully() { + + File tokenFile = new File(findWorkDir(), "minikube/hello-minikube-token"); + + KubeAuthenticationOptions options = KubeAuthenticationOptions.builder() + .role("my-role").jwtSupplier(new KubeServiceAccountTokenFile(tokenFile)) + .build(); + + RestTemplate restTemplate = TestRestTemplateFactory + .create(Settings.createSslConfiguration()); + + KubeAuthentication authentication = new KubeAuthentication(options, restTemplate); + VaultToken login = authentication.login(); + + assertThat(login.getToken()).isNotEmpty(); + } + + @Test(expected = VaultException.class) + public void loginShouldFailBadRole() { + + File tokenFile = new File(findWorkDir(), "minikube/hello-minikube-token"); + + KubeAuthenticationOptions options = KubeAuthenticationOptions.builder() + .role("wrong").jwtSupplier(new KubeServiceAccountTokenFile(tokenFile)) + .build(); + + RestTemplate restTemplate = TestRestTemplateFactory + .create(Settings.createSslConfiguration()); + + new KubeAuthentication(options, restTemplate).login(); + + } + + @Test(expected = VaultException.class) + public void loginShouldFailBadToken() { + + ClassPathResource tokenResource = new ClassPathResource("kube-jwt-token"); + + KubeAuthenticationOptions options = KubeAuthenticationOptions.builder() + .role("my-role") + .jwtSupplier(new KubeServiceAccountTokenFile(tokenResource)).build(); + + RestTemplate restTemplate = TestRestTemplateFactory + .create(Settings.createSslConfiguration()); + + new KubeAuthentication(options, restTemplate).login(); + + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationUnitTests.java new file mode 100644 index 00000000..206ef33b --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeAuthenticationUnitTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.vault.VaultException; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +/** + * Unit tests for {@link KubeAuthentication}. + * + * @author Michal Budzyn + */ +public class KubeAuthenticationUnitTests { + + private RestTemplate restTemplate; + private MockRestServiceServer mockRest; + + @Before + public void before() throws Exception { + + RestTemplate restTemplate = VaultClients.createRestTemplate(); + restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler()); + this.mockRest = MockRestServiceServer.createServer(restTemplate); + this.restTemplate = restTemplate; + } + + @Test + public void loginShouldObtainTokenWithStaticJwtSupplier() throws Exception { + + KubeAuthenticationOptions options = KubeAuthenticationOptions.builder() + .role("hello") // + .jwtSupplier(() -> "my-jwt-token").build(); + + mockRest.expect(requestTo("/auth/kubernetes/login")) + .andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.role").value("hello")) + .andExpect(jsonPath("$.jwt").value("my-jwt-token")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}")); + + KubeAuthentication authentication = new KubeAuthentication(options, restTemplate); + + VaultToken login = authentication.login(); + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + } + + @Test(expected = VaultException.class) + public void loginShouldFail() throws Exception { + + KubeAuthenticationOptions options = KubeAuthenticationOptions.builder() + .role("hello").jwtSupplier(() -> "my-jwt-token").build(); + + mockRest.expect(requestTo("/auth/kubernetes/login")) // + .andRespond(withServerError()); + + new KubeAuthentication(options, restTemplate).login(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeServiceAccountTokenFileUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeServiceAccountTokenFileUnitTests.java new file mode 100644 index 00000000..c2d93c47 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubeServiceAccountTokenFileUnitTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +/** + * Unit tests for {@link KubeServiceAccountTokenFile}. + * + * @author Michal Budzyn + */ + +public class KubeServiceAccountTokenFileUnitTests { + + private final static String TEST_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tNHcydmciLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjllMjQzNWY0LTgxNDctMTFlNy05MGFiLTA4MDAyN2NlZTQwNyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.asFRZRZ1gRj9sF0lQqbbxrNhW_lOdj9WjqUpH_4TstxqZZ7B36a0xKKXg6XaFWJY1eMsytpwa7uMzvsvf2pYCcklinaSE_F-wc42IOWcpwSLl4PND92Tp7n_JYEAbbSQVfZPzQ2Y7b6cWu6NRzDs638LwVTqYeWMWbcWlOMaTxjMzGTcgDe5RWslkKUPkYsvPOAFtt5ZErwtVcvTUmplJfHzdWwatlpZRQhYkxGgRIJ6LabXfZOd2N_TchJ3tHjAVBzUDTQq3APQssGb9df2RxVTUiyzbhdRGt7129-LCZ8rZYE7E-Mr3SSpExGYcDk-v0It8hky0CKtCLs2UHiABA"; + + @Test + public void shouldGetJwtTokenFromResource() throws Exception { + final String jwt = new KubeServiceAccountTokenFile( + new ClassPathResource("kube-jwt-token")).getKubeJwt(); + + assertThat(jwt).isEqualTo(TEST_TOKEN); + } + + @Test + public void shouldGetJwtTokenFromFile() throws Exception { + final String fileName = new ClassPathResource("kube-jwt-token").getFile() + .getAbsolutePath(); + final String jwt = new KubeServiceAccountTokenFile(fileName).getKubeJwt(); + assertThat(jwt).isEqualTo(TEST_TOKEN); + } +} \ No newline at end of file diff --git a/spring-vault-core/src/test/java/org/springframework/vault/config/EnvironmentVaultConfigurationKubeAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/config/EnvironmentVaultConfigurationKubeAuthenticationUnitTests.java new file mode 100644 index 00000000..30e48c0d --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/config/EnvironmentVaultConfigurationKubeAuthenticationUnitTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.KubeAuthentication; + +/** + * Unit tests for {@link EnvironmentVaultConfiguration} with Kube authentication. + * + * @author Michal Budzyn + */ +@RunWith(SpringRunner.class) +@TestPropertySource(properties = { "vault.uri=https://localhost:8123", + "vault.authentication=kubernetes", "vault.kubernetes.role=my-role"}) +public class EnvironmentVaultConfigurationKubeAuthenticationUnitTests { + + @Configuration + @Import(EnvironmentVaultConfiguration.class) + static class ApplicationConfiguration { + } + + @Autowired + private EnvironmentVaultConfiguration configuration; + + @Test + public void shouldConfigureAuthentication() { + + ClientAuthentication clientAuthentication = configuration.clientAuthentication(); + + assertThat(clientAuthentication).isInstanceOf(KubeAuthentication.class); + } +} diff --git a/spring-vault-core/src/test/resources/kube-jwt-token b/spring-vault-core/src/test/resources/kube-jwt-token new file mode 100644 index 00000000..2d6034c8 --- /dev/null +++ b/spring-vault-core/src/test/resources/kube-jwt-token @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tNHcydmciLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjllMjQzNWY0LTgxNDctMTFlNy05MGFiLTA4MDAyN2NlZTQwNyIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.asFRZRZ1gRj9sF0lQqbbxrNhW_lOdj9WjqUpH_4TstxqZZ7B36a0xKKXg6XaFWJY1eMsytpwa7uMzvsvf2pYCcklinaSE_F-wc42IOWcpwSLl4PND92Tp7n_JYEAbbSQVfZPzQ2Y7b6cWu6NRzDs638LwVTqYeWMWbcWlOMaTxjMzGTcgDe5RWslkKUPkYsvPOAFtt5ZErwtVcvTUmplJfHzdWwatlpZRQhYkxGgRIJ6LabXfZOd2N_TchJ3tHjAVBzUDTQq3APQssGb9df2RxVTUiyzbhdRGt7129-LCZ8rZYE7E-Mr3SSpExGYcDk-v0It8hky0CKtCLs2UHiABA \ No newline at end of file diff --git a/src/test/bash/local_run_k8s.sh b/src/test/bash/local_run_k8s.sh new file mode 100755 index 00000000..68e5d8a8 --- /dev/null +++ b/src/test/bash/local_run_k8s.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +CMD_MINIKUBE=${1:-minikube} +CMD_KUBECTL=${2:-kubectl} + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ ! -d "work" ]; then + echo "work directory could not be found." + exit 1 +fi + +mkdir -p work/minikube +SERVICE_ACCOUNT_TOKEN_FILE=work/minikube/hello-minikube-token + +function is_cluster_running() { + local _running=$(${CMD_MINIKUBE} status | grep "cluster: Running" || true) + echo "$_running" +} + +if [[ -z "$(is_cluster_running)" ]]; then + ${CMD_MINIKUBE} start --vm-driver=none --extra-config=apiserver.InsecureServingOptions.BindAddress="127.0.0.1" --extra-config=apiserver.InsecureServingOptions.BindPort="8080" + while [[ -z "$(is_cluster_running)" ]]; do + echo "Wait for minikube cluster to be up" + sleep 1 + done +fi + +export MINIKUBE_IP=$(${CMD_MINIKUBE} ip) +echo "MINIKUBE_IP ${MINIKUBE_IP}" + +# https://kubernetes.io/docs/getting-started-guides/minikube/ +${CMD_KUBECTL} run hello-minikube --image=gcr.io/google_containers/echoserver:1.4 --port=8080 +${CMD_KUBECTL} expose deployment hello-minikube --type=NodePort + +# Wait for service to be ready +echo "Wait for hello-minikube service to be ready" +HELLO_MINIKUBE_URL=$(${CMD_MINIKUBE} service hello-minikube --url) +while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${HELLO_MINIKUBE_URL})" != "200" ]]; do + sleep 3 +done + +# Copy service account token +POD_NAME=$(${CMD_KUBECTL} get pod --selector=run=hello-minikube -o jsonpath='{.items..metadata.name}') +${CMD_KUBECTL} exec ${POD_NAME} -- cat /var/run/secrets/kubernetes.io/serviceaccount/token > ${SERVICE_ACCOUNT_TOKEN_FILE} +if [ $? != 0 ] ; then + echo "Error while retrieving service account token file" + exit 1 +fi + +# Copy ca cert +cp $HOME/.minikube/ca.crt work/minikube + +#BASEDIR=`dirname $0`/../../.. +#sh <( +#cat <<-EOF +#cd ${BASEDIR} && ${BASEDIR}/src/test/bash/env.sh +#vault auth-enable kubernetes +#vault write auth/kubernetes/config kubernetes_host=https://$(minikube ip):8443 kubernetes_ca_cert=@$HOME/.minikube/ca.crt +#EOF +#) diff --git a/src/test/bash/minikube_ci_initialize.sh b/src/test/bash/minikube_ci_initialize.sh new file mode 100755 index 00000000..68cb75f6 --- /dev/null +++ b/src/test/bash/minikube_ci_initialize.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + + +# Taken from https://github.com/aaron-prindle/minikube-travis-example/blob/master/minikube-ci-initialize.sh +# Thanks to https://github.com/aaron-prindle +# +# Copyright 2017 Google, Inc. All rights reserved. +# +# 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. + +curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 && chmod +x minikube +curl -Lo kubectl https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && chmod +x kubectl + +export MINIKUBE_WANTUPDATENOTIFICATION=false +export MINIKUBE_WANTREPORTERRORPROMPT=false +export MINIKUBE_HOME=$HOME +export CHANGE_MINIKUBE_NONE_USER=true +mkdir $HOME/.kube &> /dev/null || true +touch $HOME/.kube/config + +export KUBECONFIG=$HOME/.kube/config +sudo -E ./minikube start --vm-driver=none --extra-config=apiserver.InsecureServingOptions.BindAddress="127.0.0.1" --extra-config=apiserver.InsecureServingOptions.BindPort="8080" + +# this for loop waits until kubectl can access the api server that minikube has created +KUBECTL_UP="false" +for i in {1..150} # timeout for 5 minutes +do + ./kubectl get po &> /dev/null + if [ $? -ne 1 ]; then + KUBECTL_UP="true" + break + fi + sleep 2 +done +if [ "$KUBECTL_UP" != "true" ]; then + echo "INIT FAILURE: kubectl could not reach api-server in allotted time" + exit 1 +fi +# kubectl commands are now able to interact with minikube cluster + +# OPTIONAL depending on kube-dns requirement +# this for loop waits until the kubernetes addons are active +KUBE_ADDONS_UP="false" +for i in {1..150} # timeout for 5 minutes +do + # Here we are making sure that kubectl is returning the addon pods for the namespace kube-system + # Without this check, the second if statement won't be in the proper state for execution + if [[ $(./kubectl get po -n kube-system -l k8s-app=kube-dns | tail -n +2 | grep "kube-dns") ]]; then + # Here we are taking the checking the number of running pods for the namespace kube-system + # and making sure that the value on each side of the '/' is equal (ex: 3/3 pods running) + # this is necessary to ensure that all addons have come up + if [[ ! $(./kubectl get po -n kube-system | tail -n +2 | awk '{print $2}' | grep -wEv '^([1-9]+)\/\1$') ]]; then + echo "INIT SUCCESS: all kubernetes addons pods are up and running" + KUBE_ADDONS_UP="true" + break + fi + fi + sleep 2 +done +if [ "$KUBE_ADDONS_UP" != "true" ]; then + echo "INIT FAILURE: kubernetes addons did not come up in allotted time" + exit 1 +fi +# kube-addons is available for cluster services diff --git a/src/test/bash/start.sh b/src/test/bash/start.sh index d370cc98..f7d47f31 100755 --- a/src/test/bash/start.sh +++ b/src/test/bash/start.sh @@ -12,4 +12,5 @@ pkill vault mkdir -p ${BASEDIR}/download ${BASEDIR}/src/test/bash/install_vault.sh ${BASEDIR}/src/test/bash/create_certificates.sh +${BASEDIR}/src/test/bash/local_run_k8s.sh "./minikube" "./kubectl" ${BASEDIR}/src/test/bash/local_run_vault.sh &