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:
committed by
Mark Paluch
parent
f7f9692711
commit
dca7de718c
@@ -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>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user