Add the ability to use response wrapping for AppRole secretId responses.

Original pull request: gh-165.
Closes: gh-164.
This commit is contained in:
christophetd
2017-10-24 14:49:51 +02:00
committed by Mark Paluch
parent cec3d28faf
commit a69719d573
4 changed files with 219 additions and 19 deletions

View File

@@ -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 <a href="https://www.vaultproject.io/docs/auth/approle.html">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<VaultResponse> 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) {

View File

@@ -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);
}
}
}

View File

@@ -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<String>() {
@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<String> 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");
}
});
}
}

View File

@@ -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();
}
}