From a69719d5733557a7df7edc9b4987aefea3ec86ce Mon Sep 17 00:00:00 2001 From: christophetd Date: Tue, 24 Oct 2017 14:49:51 +0200 Subject: [PATCH] Add the ability to use response wrapping for AppRole secretId responses. Original pull request: gh-165. Closes: gh-164. --- .../authentication/AppRoleAuthentication.java | 69 ++++++++++++---- .../AppRoleAuthenticationOptions.java | 37 ++++++++- ...AppRoleAuthenticationIntegrationTests.java | 78 +++++++++++++++++++ .../AppRoleAuthenticationUnitTests.java | 54 +++++++++++++ 4 files changed, 219 insertions(+), 19 deletions(-) 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 index 8aa6cb89..ce2a147a 100644 --- 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 @@ -45,6 +45,7 @@ import static org.springframework.vault.authentication.AuthenticationSteps.HttpR * * @author Mark Paluch * @author Vincent Le Nair + * @author Christophe Tafani-Dereeper * @see AppRoleAuthenticationOptions * @see RestOperations * @see Auth Backend: @@ -94,16 +95,29 @@ public class AppRoleAuthentication implements ClientAuthentication, Assert.notNull(options.getRoleId(), "RoleId must not be null for pull mode via AuthenticationSteps"); - HttpEntity body = createHttpEntity(options.getInitialToken()); + Assert.state(options.getInitialToken() != null || options.getUnwrappingToken() != null, + "One of InitialToken or UnwrappingToken must be set for pull mode via AuthenticationSteps"); - return AuthenticationSteps + AuthenticationSteps.Node secretPullRequest = null; + if (options.getInitialToken() != null) { + HttpEntity body = createHttpEntity(options.getInitialToken()); + + secretPullRequest = AuthenticationSteps .fromHttpRequest( post("auth/{mount}/role/{role}/secret-id", options.getPath(), options.getAppRole()).with(body).as( - VaultResponse.class)) - // - .map(vaultResponse -> (String) vaultResponse.getRequiredData().get( - "secret_id")) + VaultResponse.class)); + } + else { + HttpEntity body = createHttpEntity(options.getUnwrappingToken()); + secretPullRequest = AuthenticationSteps + .fromHttpRequest( + post("sys/wrapping/unwrap").with(body).as( + VaultResponse.class)); + } + + return secretPullRequest + .map(vaultResponse -> (String) vaultResponse.getRequiredData().get("secret_id")) .map(secretId -> getAppRoleLogin(options.getRoleId(), secretId)) .login("auth/{mount}/login", options.getPath()); } @@ -175,17 +189,38 @@ public class AppRoleAuthentication implements ClientAuthentication, private String getSecretId() { if (secretIdPullRequired(options)) { - try { - VaultResponse response = restOperations.postForObject( - "auth/{mount}/role/{role}/secret-id", - createHttpEntity(options.getInitialToken()), VaultResponse.class, - options.getPath(), options.getAppRole()); - return (String) response.getRequiredData().get("secret_id"); + // The secret ID needs to be pulled from Vault. + // Case 1: we use the initial authentication token + if (options.getInitialToken() != null) { + try { + VaultResponse response = restOperations.postForObject( + "auth/{mount}/role/{role}/secret-id", + createHttpEntity(options.getInitialToken()), VaultResponse.class, + options.getPath(), options.getAppRole()); + return (String) response.getRequiredData().get("secret_id"); + } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format( + "Cannot get Secret id using AppRole: %s", + VaultResponses.getError(e.getResponseBodyAsString()))); + } } - catch (HttpStatusCodeException e) { - throw new VaultException(String.format( - "Cannot get Secret id using AppRole: %s", - VaultResponses.getError(e.getResponseBodyAsString()))); + // Case 2: the secret ID needs to be unwrapped + else if (options.getUnwrappingToken() != null) { + try { + VaultResponse response = restOperations.postForObject( + "sys/wrapping/unwrap", + createHttpEntity(options.getUnwrappingToken()), VaultResponse.class, + options.getPath(), options.getAppRole()); + + return (String) response.getRequiredData().get("secret_id"); + } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format( + "Cannot unwrap Secret id using AppRole: %s", + VaultResponses.getError(e.getResponseBodyAsString()) + )); + } } } @@ -193,7 +228,7 @@ public class AppRoleAuthentication implements ClientAuthentication, } private static boolean secretIdPullRequired(AppRoleAuthenticationOptions options) { - return options.getSecretId() == null && options.getInitialToken() != null; + return options.getSecretId() == null && (options.getInitialToken() != null || options.getUnwrappingToken() != null); } private static HttpEntity createHttpEntity(VaultToken token) { 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 index 61bceeff..c32d4afd 100644 --- 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 @@ -65,15 +65,22 @@ public class AppRoleAuthenticationOptions { @Nullable private final VaultToken initialToken; + /** + * Token for unwrapping the secretId response + */ + @Nullable + private final VaultToken unwrappingToken; + private AppRoleAuthenticationOptions(String path, @Nullable String roleId, @Nullable String secretId, @Nullable String appRole, - @Nullable VaultToken initialToken) { + @Nullable VaultToken initialToken, @Nullable VaultToken unwrappingToken) { this.path = path; this.roleId = roleId; this.secretId = secretId; this.appRole = appRole; this.initialToken = initialToken; + this.unwrappingToken = unwrappingToken; } /** @@ -124,6 +131,15 @@ public class AppRoleAuthenticationOptions { return initialToken; } + /** + * @return the token used to unwrap the roleId response. + * @since 2.0 + */ + @Nullable + public VaultToken getUnwrappingToken() { + return unwrappingToken; + } + /** * Builder for {@link AppRoleAuthenticationOptions}. */ @@ -141,6 +157,8 @@ public class AppRoleAuthenticationOptions { private VaultToken initialToken; + private VaultToken unwrappingToken; + AppRoleAuthenticationOptionsBuilder() { } @@ -217,6 +235,21 @@ public class AppRoleAuthenticationOptions { return this; } + /** + * Configure a {@code unwrappingToken}. + * + * @param unwrappingToken must not be empty or {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + * @since 2.0 + */ + public AppRoleAuthenticationOptionsBuilder unwrappingToken(VaultToken unwrappingToken) { + + Assert.notNull(unwrappingToken, "UnwrappingToken must not be null"); + + this.unwrappingToken = unwrappingToken; + return this; + } + /** * Build a new {@link AppRoleAuthenticationOptions} instance. Requires * {@link #roleId(String)} for push mode or {@link #appRole(String)} and @@ -247,7 +280,7 @@ public class AppRoleAuthenticationOptions { } return new AppRoleAuthenticationOptions(path, roleId, secretId, appRole, - initialToken); + initialToken, unwrappingToken); } } } 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 index e25f51b5..3b1201c5 100644 --- 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 @@ -22,11 +22,18 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; import org.springframework.vault.VaultException; +import org.springframework.vault.core.RestOperationsCallback; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.Settings; +import org.springframework.web.client.RestOperations; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.anyOf; @@ -40,6 +47,7 @@ import static org.junit.Assume.assumeThat; * Integration tests for {@link AppRoleAuthentication}. * * @author Mark Paluch + * @author Christophe Tafani-Dereeper */ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSupport { @@ -128,6 +136,56 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor assertThat(authentication.login()).isNotNull(); } + @Test + public void shouldAuthenticateWithWrappedSecretId() { + String roleId = getRoleId("no-secret-id"); + // Simulate that an operator / CM tool created a wrapped secret ID response before the application starts up + String unwrappingToken = generateWrappedSecretIdResponse(); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .unwrappingToken(VaultToken.of(unwrappingToken)) + .roleId(roleId) + .build(); + + AppRoleAuthentication authentication = new AppRoleAuthentication(options, + prepare().getRestTemplate()); + + assertThat(authentication.login()).isNotNull(); + } + + @Test(expected = VaultException.class) + public void shouldAuthenticateWithWrappedSecretIdFailIfUnwrappingTokenExpired() { + String roleId = getRoleId("no-secret-id"); + String unwrappingToken = "incorrect-unwrapping-token"; + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .unwrappingToken(VaultToken.of(unwrappingToken)) + .roleId(roleId) + .build(); + + AppRoleAuthentication authentication = new AppRoleAuthentication(options, + prepare().getRestTemplate()); + + authentication.login(); + } + + @Test + public void authenticationStepsShouldAuthenticateWithWrappedSecretId() { + String roleId = getRoleId("no-secret-id"); + String unwrappingToken = generateWrappedSecretIdResponse(); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .unwrappingToken(VaultToken.of(unwrappingToken)) + .roleId(roleId) + .build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + @Test(expected = VaultException.class) public void shouldAuthenticatePullModeFailsWithoutSecretId() { @@ -240,4 +298,24 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor .read(String.format("auth/approle/role/%s/role-id", roleName)).getData() .get("role_id"); } + + @Nullable + private String generateWrappedSecretIdResponse() { + return getVaultOperations().doWithVault(new RestOperationsCallback() { + @Nullable + @Override + public String doWithRestOperations(RestOperations restOperations) { + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Vault-Wrap-Ttl", "3600"); + headers.set("X-Vault-Token", Settings.token().getToken()); + HttpEntity httpEntity = new HttpEntity<>(null, headers); + + VaultResponse response = restOperations.exchange("auth/approle/role/with-secret-id/secret-id", + HttpMethod.PUT, httpEntity, VaultResponse.class).getBody(); + + return response.getWrapInfo().get("token"); + } + }); + + } } 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 index ae785993..c816c63a 100644 --- 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 @@ -27,6 +27,7 @@ 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.bind.annotation.RequestMethod; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -42,6 +43,7 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat * * @author Mark Paluch * @author Vincent Le Nair + * @author Christophe Tafani-Dereeper */ public class AppRoleAuthenticationUnitTests { @@ -174,4 +176,56 @@ public class AppRoleAuthenticationUnitTests { new AppRoleAuthentication(options, restTemplate).login(); } + + @Test + public void loginShouldUnwrapSecretIdResponse() { + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId("my_role_id") + .unwrappingToken(VaultToken.of("unwrapping_token")) + .build(); + + // Expect a first request to unwrap the response + mockRest.expect(requestTo("/sys/wrapping/unwrap")) + .andExpect(header("X-Vault-Token", "unwrapping_token")) + .andExpect(method(HttpMethod.POST)) + .andRespond( + withSuccess().contentType(MediaType.APPLICATION_JSON).body("{" + + " \"request_id\": \"aad6a19b-a42b-b750-cafb-51087662f53e\"," + + " \"lease_id\": \"\"," + + " \"renewable\": false," + + " \"lease_duration\": 0," + + " \"data\": {" + + " \"secret_id\": \"my_secret_id\"," + + " \"secret_id_accessor\": \"my_secret_id_accessor\"" + + " }," + + " \"wrap_info\": null," + + " \"warnings\": null," + + " \"auth\": null" + + "}" + ) + ); + + // Also expect a second request to retrieve a token + mockRest.expect(requestTo("/auth/approle/login")) + .andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.role_id").value("my_role_id")) + .andExpect(jsonPath("$.secret_id").value("my_secret_id")) + .andRespond( + withSuccess() + .contentType(MediaType.APPLICATION_JSON) + .body("{" + + "\"auth\":{\"client_token\":\"my-token\", \"lease_duration\": 10, \"renewable\": true}" + + "}") + ); + + AppRoleAuthentication auth = new AppRoleAuthentication(options, restTemplate); + + VaultToken login = auth.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + assertThat(((LoginToken) login).getLeaseDuration()).isEqualTo( + Duration.ofSeconds(10)); + assertThat(((LoginToken) login).isRenewable()).isTrue(); + } }