Support AppRole authentication pull mode.

We now support AppRole authentication pull mode by fetching roleId/secretId from Vault's AppRole auth backend using an initial (ephemeral token) if roleId/secretId are not configured.

Original pull request: gh-133.
Related ticket: gh-132.
This commit is contained in:
Vincent Le Nair
2017-09-06 14:02:29 +01:00
committed by Mark Paluch
parent f7f9692711
commit dca7de718c
4 changed files with 216 additions and 20 deletions

View File

@@ -17,11 +17,14 @@ 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.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.VaultException;
import org.springframework.vault.client.VaultResponses;
import org.springframework.vault.support.VaultResponse;
@@ -67,19 +70,21 @@ public class AppRoleAuthentication implements ClientAuthentication {
this.restOperations = restOperations;
}
@Override
public VaultToken login() {
@Override public VaultToken login() {
return createTokenUsingAppRole();
}
private VaultToken createTokenUsingAppRole() {
Map<String, String> login = getAppRoleLogin(options.getRoleId(),
options.getSecretId());
String roleId = getRoleId();
String secretId = getSecretId();
Map<String, String> login = getAppRoleLogin(roleId, secretId);
try {
VaultResponse response = restOperations.postForObject("auth/{mount}/login",
login, VaultResponse.class, options.getPath());
VaultResponse response = restOperations
.postForObject("auth/{mount}/login", login, VaultResponse.class,
options.getPath());
logger.debug("Login successful using AppRole authentication");
@@ -87,10 +92,58 @@ public class AppRoleAuthentication implements ClientAuthentication {
}
catch (HttpStatusCodeException e) {
throw new VaultException(String.format("Cannot login using AppRole: %s",
VaultResponses.getError(e.getResponseBodyAsString())));
VaultResponses.getError(e.getResponseBodyAsString())));
}
}
private String getRoleId() {
String roleId = options.getRoleId();
if (StringUtils.isEmpty(roleId) && !StringUtils.isEmpty(options.getAppRole())) {
try {
ResponseEntity<VaultResponse> response = restOperations
.exchange("auth/approle/role/{role}/role-id", HttpMethod.GET,
createHttpEntityWithToken(), VaultResponse.class,
options.getAppRole());
roleId = (String) response.getBody().getData().get("role_id");
}
catch (HttpStatusCodeException e) {
throw new VaultException(String
.format("Cannot get Role id using AppRole: %s",
VaultResponses.getError(e.getResponseBodyAsString())));
}
}
return roleId;
}
private String getSecretId() {
String secretId = options.getSecretId();
if (StringUtils.isEmpty(secretId) && !StringUtils.isEmpty(options.getAppRole())) {
try {
VaultResponse response = restOperations
.postForObject("auth/approle/role/{role}/secret-id",
createHttpEntityWithToken(), VaultResponse.class,
options.getAppRole());
secretId = (String) response.getData().get("secret_id");
}
catch (HttpStatusCodeException e) {
throw new VaultException(String
.format("Cannot get Secret id using AppRole: %s",
VaultResponses.getError(e.getResponseBodyAsString())));
}
}
return secretId;
}
private HttpEntity createHttpEntityWithToken() {
HttpHeaders headers = new HttpHeaders();
if (options.getInitialToken() != null) {
headers.set("X-Vault-Token", options.getInitialToken());
}
return new HttpEntity<String>(null, headers);
}
private Map<String, String> getAppRoleLogin(String roleId, String secretId) {
Map<String, String> login = new HashMap<String, String>();

View File

@@ -16,6 +16,7 @@
package org.springframework.vault.authentication;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Authentication options for {@link AppRoleAuthentication}.
@@ -47,11 +48,23 @@ public class AppRoleAuthenticationOptions {
*/
private final String secretId;
private AppRoleAuthenticationOptions(String path, String roleId, String secretId) {
/**
* Role name used to get roleId and secretID
*/
private final String appRole;
/**
* Token associated to the roleName.
*/
private final String initialToken;
private AppRoleAuthenticationOptions(String path, String roleId, String secretId, String appRole, String initialToken) {
this.path = path;
this.roleId = roleId;
this.secretId = secretId;
this.appRole = appRole;
this.initialToken = initialToken;
}
/**
@@ -82,6 +95,20 @@ public class AppRoleAuthenticationOptions {
return secretId;
}
/**
* @return the bound AppRole.
*/
public String getAppRole() {
return appRole;
}
/**
* @return the bound InitialToken.
*/
public String getInitialToken() {
return initialToken;
}
/**
* Builder for {@link AppRoleAuthenticationOptions}.
*/
@@ -89,6 +116,10 @@ public class AppRoleAuthenticationOptions {
private String path = DEFAULT_APPROLE_AUTHENTICATION_PATH;
private String appRole;
private String initialToken;
private String roleId;
private String secretId;
@@ -111,6 +142,34 @@ public class AppRoleAuthenticationOptions {
return this;
}
/**
* Configure a {@code appRole}.
*
* @param appRole must not be empty or {@literal null}.
* @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}.
*/
public AppRoleAuthenticationOptionsBuilder appRole(String appRole) {
Assert.hasText(appRole, "AppRole must not be empty");
this.appRole = appRole;
return this;
}
/**
* Configure a {@code initialToken}.
*
* @param initialToken must not be empty or {@literal null}.
* @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}.
*/
public AppRoleAuthenticationOptionsBuilder initialToken(String initialToken) {
Assert.hasText(initialToken, "InitialToken must not be empty");
this.initialToken = initialToken;
return this;
}
/**
* Configure the RoleId.
*
@@ -141,16 +200,28 @@ public class AppRoleAuthenticationOptions {
/**
* Build a new {@link AppRoleAuthenticationOptions} instance. Requires
* {@link #roleId(String)} to be configured.
* {@link #roleId(String)} for Push Mode or {@link #appRole(String)} and
* {@link #initialToken(String)} for pull Mode 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);
//Role ID is required in order to use push mode (no appRole and initialToken)
if (StringUtils.isEmpty(appRole) && StringUtils.isEmpty(initialToken)) {
Assert.notNull(roleId, "RoleId must not be null");
}
//AppRole and InitialToken are required in order to use pull mode (no roleId)
if (StringUtils.isEmpty(roleId)) {
Assert.notNull(appRole, "AppRole must not be null");
Assert.notNull(initialToken, "InitialToken must not be null");
}
return new AppRoleAuthenticationOptions(path, roleId, secretId, appRole,
initialToken);
}
}
}

View File

@@ -28,9 +28,7 @@ import org.springframework.vault.support.VaultToken;
import org.springframework.web.client.RestTemplate;
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.match.MockRestRequestMatchers.*;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
@@ -78,6 +76,57 @@ public class AppRoleAuthenticationUnitTests {
assertThat(login.getToken()).isEqualTo("my-token");
}
@Test
public void loginShouldPullRoleIdAndSecretId() throws Exception {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder()
.appRole("app_role")
.initialToken("initial_token")
.build();
mockRest.expect(requestTo("/auth/approle/role/app_role/role-id"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("X-Vault-token", "initial_token"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body(
"{\"data\": {\"role_id\": \"hello\"}}"
));
mockRest.expect(requestTo("/auth/approle/role/app_role/secret-id"))
.andExpect(method(HttpMethod.POST))
.andExpect(header("X-Vault-token", "initial_token"))
.andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON).body(
"{\"data\": {\"secret_id\": \"world\"}}"
));
mockRest.expect(requestTo("/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, restTemplate);
VaultToken login = sut.login();
assertThat(login).isInstanceOf(LoginToken.class);
assertThat(login.getToken()).isEqualTo("my-token");
}
@Test(expected = IllegalArgumentException.class)
public void loginShouldFailIfPullModeButNoToken() throws Exception {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder()
.appRole("app_role")
.build();
AppRoleAuthentication sut = new AppRoleAuthentication(options, restTemplate);
sut.login();
}
@Test
public void loginShouldObtainTokenWithoutSecretId() throws Exception {

View File

@@ -196,10 +196,7 @@ authentication, like the deprecated (since Vault 0.6.1) <<vault.authentication.a
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.
or together with a provided SecretId.
====
[source,java]
@@ -225,6 +222,32 @@ class AppConfig extends AbstractVaultConfiguration {
----
====
Spring Vault also support a full pull mode: if RoleId and SecretId are not provided, Spring Vault will retreive them usind AppRole and RoleToken
====
[source,java]
----
@Configuration
class AppConfig extends AbstractVaultConfiguration {
// …
@Override
public ClientAuthentication clientAuthentication() {
AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder()
.appRole("…")
.roleToken("…")
.build();
return new AppRoleAuthentication(options, restOperations());
}
// …
}
----
====
See also: https://www.vaultproject.io/docs/auth/approle.html[Vault Documentation: Using the AppRole auth backend]
[[vault.authentication.awsec2]]