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 2da3832b..77a1d084 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 @@ -17,12 +17,16 @@ 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.lang.Nullable; 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; @@ -87,8 +91,7 @@ public class AppRoleAuthentication implements ClientAuthentication, .login("auth/{mount}/login", options.getPath()); } - @Override - public VaultToken login() { + @Override public VaultToken login() { return createTokenUsingAppRole(); } @@ -99,12 +102,15 @@ public class AppRoleAuthentication implements ClientAuthentication, private VaultToken createTokenUsingAppRole() { - Map login = getAppRoleLogin(options.getRoleId(), - options.getSecretId()); + String roleId = getRoleId(); + String secretId = getSecretId(); + + Map 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()); Assert.state(response != null && response.getAuth() != null, "Auth field must not be null"); @@ -115,10 +121,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 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(null, headers); + } + private static Map getAppRoleLogin(String roleId, @Nullable String secretId) { 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 5f52fbcc..49be9170 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 @@ -17,6 +17,7 @@ package org.springframework.vault.authentication; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Authentication options for {@link AppRoleAuthentication}. @@ -49,12 +50,24 @@ public class AppRoleAuthenticationOptions { @Nullable private final 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, - @Nullable String secretId) { + @Nullable String secretId, String appRole, String initialToken) { this.path = path; this.roleId = roleId; this.secretId = secretId; + this.appRole = appRole; + this.initialToken = initialToken; } /** @@ -86,6 +99,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}. */ @@ -93,6 +120,10 @@ public class AppRoleAuthenticationOptions { private String path = DEFAULT_APPROLE_AUTHENTICATION_PATH; + private String appRole; + + private String initialToken; + @Nullable private String roleId; @@ -117,6 +148,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. * @@ -147,16 +206,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); } } } 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 97d406d2..8de25f84 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 @@ -30,9 +30,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; @@ -80,6 +78,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() { diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc index da8aa30b..450c5f26 100644 --- a/src/main/asciidoc/reference/authentication.adoc +++ b/src/main/asciidoc/reference/authentication.adoc @@ -196,10 +196,7 @@ authentication, like the deprecated (since Vault 0.6.1) <