diff --git a/docs/index.html b/docs/index.html index a619cebb..f02dc08a 100755 --- a/docs/index.html +++ b/docs/index.html @@ -49,6 +49,7 @@ high-level abstractions for interacting with Vault, freeing the user from infras * Connection package as low-level abstraction * Reading, writing and deleting data from Vault with object mapping support +* Multiple authentication mechanisms: AppId, AppRole, Client Certificates, and Cubbyhole (wrapped/stored token) @@ -109,4 +110,4 @@ public class Example { {% include project_page.html %} - \ No newline at end of file + diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java new file mode 100644 index 00000000..332538fa --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthentication.java @@ -0,0 +1,95 @@ +/* + * 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.vault.authentication; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; +import org.springframework.vault.client.VaultClient; +import org.springframework.vault.client.VaultException; +import org.springframework.vault.client.VaultResponseEntity; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; + +/** + * AppRole implementation of {@link ClientAuthentication}. RoleId and SecretId (optional) are sent in the login request + * to Vault to obtain a {@link VaultToken}. + *

+ * {@link AppRoleAuthentication} can be configured for push and pull mode by setting + * {@link AppRoleAuthenticationOptions#getSecretId()}. + * + * @author Mark Paluch + * @see AppRoleAuthenticationOptions + * @see VaultClient + * @see Auth Backend: AppRole + */ +public class AppRoleAuthentication implements ClientAuthentication { + + private final static Logger logger = LoggerFactory.getLogger(AppRoleAuthentication.class); + + private final AppRoleAuthenticationOptions options; + + private final VaultClient vaultClient; + + /** + * Creates a {@link AppRoleAuthentication} using {@link AppRoleAuthenticationOptions} and {@link VaultClient}. + * + * @param options must not be {@literal null}. + * @param vaultClient must not be {@literal null}. + */ + public AppRoleAuthentication(AppRoleAuthenticationOptions options, VaultClient vaultClient) { + + Assert.notNull(options, "AppRoleAuthenticationOptions must not be null"); + Assert.notNull(vaultClient, "VaultClient must not be null"); + + this.options = options; + this.vaultClient = vaultClient; + } + + @Override + public VaultToken login() { + return createTokenUsingAppRole(); + } + + private VaultToken createTokenUsingAppRole() { + + Map login = getAppRoleLogin(options.getRoleId(), options.getSecretId()); + + VaultResponseEntity entity = vaultClient + .postForEntity(String.format("auth/%s/login", options.getPath()), login, VaultResponse.class); + + if (!entity.isSuccessful()) { + throw new VaultException(String.format("Cannot login using AppRole: %s", entity.getMessage())); + } + + logger.debug("Login successful using AppRole authentication"); + + return LoginTokenUtil.from(entity.getBody().getAuth()); + } + + private Map getAppRoleLogin(String roleId, String secretId) { + + Map login = new HashMap(); + login.put("role_id", roleId); + if (secretId != null) { + login.put("secret_id", secretId); + } + return login; + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthenticationOptions.java new file mode 100644 index 00000000..4e1850cc --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleAuthenticationOptions.java @@ -0,0 +1,153 @@ +/* + * 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.vault.authentication; + +import org.springframework.util.Assert; + +/** + * Authentication options for {@link AppRoleAuthentication}. + *

+ * Authentication options provide the path, roleId and pull/push mode. {@link AppRoleAuthentication} can be + * constructed using {@link #builder()}. Instances of this class are immutable once constructed. + * + * @author Mark Paluch + * @see AppRoleAuthentication + * @see #builder() + */ +public class AppRoleAuthenticationOptions { + + public final static String DEFAULT_APPROLE_AUTHENTICATION_PATH = "approle"; + + /** + * Path of the apprile authentication backend mount. + */ + private final String path; + + /** + * The RoleId. + */ + private final String roleId; + + /** + * The Bind SecretId. + */ + private final String secretId; + + private AppRoleAuthenticationOptions(String path, String roleId, String secretId) { + + this.path = path; + this.roleId = roleId; + this.secretId = secretId; + } + + /** + * @return a new {@link AppRoleAuthenticationOptionsBuilder}. + */ + public static AppRoleAuthenticationOptionsBuilder builder() { + return new AppRoleAuthenticationOptionsBuilder(); + } + + /** + * @return the mount path. + */ + public String getPath() { + return path; + } + + /** + * @return the RoleId. + */ + public String getRoleId() { + return roleId; + } + + /** + * @return the bound SecretId. + */ + public String getSecretId() { + return secretId; + } + + /** + * Builder for {@link AppRoleAuthenticationOptions}. + */ + public static class AppRoleAuthenticationOptionsBuilder { + + private String path = DEFAULT_APPROLE_AUTHENTICATION_PATH; + + private String roleId; + + private String secretId; + + AppRoleAuthenticationOptionsBuilder() {} + + /** + * Configure the mount path. + * + * @param path must not be empty or {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + * @see #DEFAULT_APPROLE_AUTHENTICATION_PATH + */ + public AppRoleAuthenticationOptionsBuilder path(String path) { + + Assert.hasText(path, "Path must not be empty"); + + this.path = path; + return this; + } + + /** + * Configure the RoleId. + * + * @param roleId must not be empty or {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + */ + public AppRoleAuthenticationOptionsBuilder roleId(String roleId) { + + Assert.hasText(roleId, "RoleId must not be empty"); + + this.roleId = roleId; + return this; + } + + /** + * Configure a {@code secretId}. + * + * @param secretId must not be empty or {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + */ + public AppRoleAuthenticationOptionsBuilder secretId(String secretId) { + + Assert.hasText(secretId, "SecretId must not be empty"); + + this.secretId = secretId; + return this; + } + + /** + * Build a new {@link AppRoleAuthenticationOptions} instance. Requires {@link #roleId(String)} to be configured. + * + * @return a new {@link AppRoleAuthenticationOptions}. + */ + public AppRoleAuthenticationOptions build() { + + Assert.hasText(path, "Path must not be empty"); + Assert.notNull(roleId, "RoleId must not be null"); + + return new AppRoleAuthenticationOptions(path, roleId, secretId); + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java new file mode 100644 index 00000000..532e3a06 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java @@ -0,0 +1,150 @@ +/* + * 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.vault.authentication; + +import static org.assertj.core.api.Assertions.*; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assume.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.vault.client.VaultException; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.util.IntegrationTestSupport; + +/** + * Integration tests for {@link AppRoleAuthentication}. + * + * @author Mark Paluch + */ +public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSupport { + + @Before + public void before() { + + assumeThat(prepare().getVaultOperations().opsForSys().health().getVersion(), + not(anyOf(nullValue(), equalTo(""), containsString("0.5"), containsString("0.6.1")))); + + if (!prepare().hasAuth("approle")) { + prepare().mountAuth("approle"); + } + + getVaultOperations().doWithVault(new VaultOperations.SessionCallback() { + + @Override + public Object doWithVault(VaultOperations.VaultSession session) { + + Map withSecretId = new HashMap(); + withSecretId.put("policies", "dummy"); // policy + withSecretId.put("bound_cidr_list", "0.0.0.0/0"); + withSecretId.put("bind_secret_id", "true"); + + session.postForEntity("auth/approle/role/with-secret-id", withSecretId, Map.class); + + Map noSecretIdRole = new HashMap(); + noSecretIdRole.put("policies", "dummy"); // policy + noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); + noSecretIdRole.put("bind_secret_id", "false"); + + session.postForEntity("auth/approle/role/no-secret-id", noSecretIdRole, Map.class); + + return null; + } + }); + } + + @Test + public void shouldAuthenticateWithRoleIdOnly() { + + String roleId = getRoleId("no-secret-id"); + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient()); + + assertThat(authentication.login()).isNotNull(); + } + + @Test + public void shouldAuthenticatePullModeWithGeneratedSecretId() { + + String roleId = getRoleId("with-secret-id"); + String secretId = (String) getVaultOperations() + .write(String.format("auth/approle/role/%s/secret-id", "with-secret-id"), null).getData().get("secret_id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).secretId(secretId) + .build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient()); + + assertThat(authentication.login()).isNotNull(); + } + + @Test(expected = VaultException.class) + public void shouldAuthenticatePullModeFailsWithoutSecretId() { + + String roleId = getRoleId("with-secret-id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient()); + + assertThat(authentication.login()).isNotNull(); + } + + @Test(expected = VaultException.class) + public void shouldAuthenticatePullModeFailsWithWrongSecretId() { + + String roleId = getRoleId("with-secret-id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId) + .secretId("this-is-a-wrong-secret-id").build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient()); + + assertThat(authentication.login()).isNotNull(); + } + + @Test + public void shouldAuthenticatePushModeWithProvidedSecretId() { + + String roleId = getRoleId("with-secret-id"); + final String secretId = "hello_world"; + + final VaultResponse customSecretIdResponse = getVaultOperations() + .write("auth/approle/role/with-secret-id/custom-secret-id", Collections.singletonMap("secret_id", secretId)); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId(roleId).secretId(secretId) + .build(); + AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getVaultClient()); + + assertThat(authentication.login()).isNotNull(); + + getVaultOperations().write("auth/approle/role/with-secret-id/secret-id-accessor/destroy", + customSecretIdResponse.getData()); + } + + private VaultOperations getVaultOperations() { + return prepare().getVaultOperations(); + } + + private String getRoleId(String roleName) { + return (String) getVaultOperations().read(String.format("auth/approle/role/%s/role-id", roleName)).getData() + .get("role_id"); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java new file mode 100644 index 00000000..3128ebae --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationUnitTests.java @@ -0,0 +1,107 @@ +/* + * 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.vault.authentication; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.*; + +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.client.VaultClient; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.VaultException; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +/** + * Unit tests for {@link AppRoleAuthentication}. + * + * @author Mark Paluch + */ +public class AppRoleAuthenticationUnitTests { + + private VaultClient vaultClient; + private MockRestServiceServer mockRest; + + @Before + public void before() throws Exception { + + RestTemplate restTemplate = new RestTemplate(); + mockRest = MockRestServiceServer.createServer(restTemplate); + vaultClient = new VaultClient(restTemplate, new VaultEndpoint()); + } + + @Test + public void loginShouldObtainToken() throws Exception { + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId("hello") // + .secretId("world") // + .build(); + + mockRest.expect(requestTo("https://localhost:8200/v1/auth/approle/login")) // + .andExpect(method(HttpMethod.POST)) // + .andExpect(jsonPath("$.role_id").value("hello")) // + .andExpect(jsonPath("$.secret_id").value("world")) // + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}")); + + AppRoleAuthentication sut = new AppRoleAuthentication(options, vaultClient); + + VaultToken login = sut.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + } + + @Test + public void loginShouldObtainTokenWithoutSecretId() throws Exception { + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId("hello") // + .build(); + + mockRest.expect(requestTo("https://localhost:8200/v1/auth/approle/login")) // + .andExpect(method(HttpMethod.POST)) // + .andExpect(jsonPath("$.role_id").value("hello")) // + .andExpect(jsonPath("$.secret_id").doesNotExist()) // + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{" + "\"auth\":{\"client_token\":\"my-token\", \"lease_duration\": 10, \"renewable\": true}" + "}")); + + AppRoleAuthentication sut = new AppRoleAuthentication(options, vaultClient); + + VaultToken login = sut.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo(10); + assertThat(((LoginToken) login).isRenewable()).isTrue(); + } + + @Test(expected = VaultException.class) + public void loginShouldFail() throws Exception { + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder().roleId("hello") // + .build(); + + mockRest.expect(requestTo("https://localhost:8200/v1/auth/approle/login")) // + .andRespond(withServerError()); + + new AppRoleAuthentication(options, vaultClient).login(); + } +} diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc index 9143fc7e..633e7bc6 100644 --- a/src/main/asciidoc/reference/authentication.adoc +++ b/src/main/asciidoc/reference/authentication.adoc @@ -34,6 +34,7 @@ class AppConfig extends AbstractVaultConfiguration { See also: https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens] +[[vault.authentication.appid]] == AppId authentication Vault supports https://www.vaultproject.io/docs/auth/app-id.html[AppId] @@ -143,6 +144,44 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism { See also: https://www.vaultproject.io/docs/auth/app-id.html[Vault Documentation: Using the App ID auth backend] +== AppRole authentication + +https://www.vaultproject.io/docs/auth/app-id.html[AppRole] allows machine +authentication, like the deprecated (since Vault 0.6.1) <>. +AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId. + +Spring Vault supports AppRole authentication by providing either RoleId only +or together with a provided SecretId (push or pull mode). + +RoleId and optionally SecretId must be provided to `AppRoleAuthenticationOptions`, +Spring Vault will not look up these or create a custom SecretId. + +==== +[source,java] +---- +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId("…") + .secretId("…") + .build(); + + return new AppRoleAuthentication(options, vaultClient()); + } + + // … +} +---- +==== + +See also: https://www.vaultproject.io/docs/auth/approle.html[Vault Documentation: Using the AppRole auth backend] + == AWS-EC2 authentication The https://www.vaultproject.io/docs/auth/aws-ec2.html[aws-ec2] @@ -335,4 +374,4 @@ See also: * https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens] * https://www.vaultproject.io/docs/secrets/cubbyhole/index.html[Vault Documentation:Cubbyhole Secret Backend] -* https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping] \ No newline at end of file +* https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping]