Add the ability to use response wrapping for AppRole secretId responses.
Original pull request: gh-165. Closes: gh-164.
This commit is contained in:
committed by
Mark Paluch
parent
cec3d28faf
commit
a69719d573
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user