From cfb84fe2b5f40465a904980114d659f4a54a504f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 25 Oct 2017 15:11:58 +0200 Subject: [PATCH] Encapsulate RoleId and SecretId modes for AppRole authentication. Encapsulate RoleId and SecretId modes (pull, provided, wrapped, absent) with value objects. Adapt AppRoleAuthentication for imperative and AuthenticationSteps authentication. Split imperative and AuthenticationSteps tests. Use the deprecated cubbyhole response unwrapping endpoint to unwrap responses. Original pull request: gh-165. Closes: gh-165. --- .../authentication/AppRoleAuthentication.java | 288 ++++++++++++------ .../AppRoleAuthenticationOptions.java | 268 ++++++++++++---- .../vault/authentication/AppRoleTokens.java | 70 +++++ ...RoleAuthenticationIntegrationTestBase.java | 124 ++++++++ ...AppRoleAuthenticationIntegrationTests.java | 191 ++---------- ...leAuthenticationStepsIntegrationTests.java | 165 ++++++++++ .../AppRoleAuthenticationUnitTests.java | 41 +-- src/main/asciidoc/new-features.adoc | 1 + .../asciidoc/reference/authentication.adoc | 10 +- 9 files changed, 828 insertions(+), 330 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleTokens.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java 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 ce2a147a..2fa11c56 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 @@ -27,13 +27,22 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; +import org.springframework.vault.authentication.AppRoleTokens.AbsentSecretId; +import org.springframework.vault.authentication.AppRoleTokens.Provided; +import org.springframework.vault.authentication.AppRoleTokens.Pull; +import org.springframework.vault.authentication.AppRoleTokens.Wrapped; +import org.springframework.vault.authentication.AuthenticationSteps.Node; import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestOperations; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.get; import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; /** @@ -90,41 +99,104 @@ public class AppRoleAuthentication implements ClientAuthentication, Assert.notNull(options, "AppRoleAuthenticationOptions must not be null"); - if (secretIdPullRequired(options)) { + RoleId roleId = options.getRoleId(); + SecretId secretId = options.getSecretId(); - Assert.notNull(options.getRoleId(), - "RoleId must not be null for pull mode via AuthenticationSteps"); + if ((roleId instanceof Wrapped || roleId instanceof Pull) + && (secretId instanceof Wrapped || secretId instanceof Pull)) { - Assert.state(options.getInitialToken() != null || options.getUnwrappingToken() != null, - "One of InitialToken or UnwrappingToken must be set for pull mode via AuthenticationSteps"); - - AuthenticationSteps.Node 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)); - } - 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()); + throw new IllegalArgumentException( + "RoleId and SecretId are both configured to obtain their values from initial Vault request. AuthenticationSteps supports currently only fetching of a single element."); } - return AuthenticationSteps.fromSupplier( - () -> getAppRoleLogin(options.getRoleId(), options.getSecretId())) // - .login("auth/{mount}/login", options.getPath()); + return getAuthenticationSteps(options, roleId, secretId).login( + "auth/{mount}/login", options.getPath()); + } + + private static Node getAuthenticationSteps(AppRoleAuthenticationOptions options, + RoleId roleId, SecretId secretId) { + + if (roleId instanceof Pull || roleId instanceof Wrapped) { + + Node steps; + + if (roleId instanceof Pull) { + + HttpHeaders headers = createHttpHeaders(((Pull) roleId).getInitialToken()); + + steps = AuthenticationSteps.fromHttpRequest(get( + "auth/{mount}/role/{role}/role-id", options.getPath(), + options.getAppRole()).with(headers).as(VaultResponse.class)); + } + else { + steps = unwrapResponse(((Wrapped) roleId).getInitialToken()); + } + + return steps.map( + vaultResponse -> (String) vaultResponse.getRequiredData().get( + "role_id")).map( + roleIdToken -> { + + return getAppRoleLoginBody( + roleIdToken, + secretId instanceof Provided ? ((Provided) secretId) + .getValue() : null); + }); + } + + if (secretId instanceof Pull || secretId instanceof Wrapped) { + + Node steps; + + if (secretId instanceof Pull) { + HttpHeaders headers = createHttpHeaders(((Pull) secretId) + .getInitialToken()); + + steps = AuthenticationSteps.fromHttpRequest(post( + "auth/{mount}/role/{role}/secret-id", options.getPath(), + options.getAppRole()).with(headers).as(VaultResponse.class)); + } + else { + steps = unwrapResponse(((Wrapped) secretId).getInitialToken()); + } + + return steps.map( + vaultResponse -> (String) vaultResponse.getRequiredData().get( + "secret_id")).map( + secretIdToken -> { + + return getAppRoleLoginBody( + roleId instanceof Provided ? ((Provided) roleId) + .getValue() : null, secretIdToken); + }); + } + + if (roleId instanceof Provided) { + + return AuthenticationSteps.fromSupplier(() -> { + + return getAppRoleLoginBody(((Provided) roleId).getValue(), + secretId instanceof Provided ? ((Provided) secretId).getValue() + : null); + }); + } + + throw new IllegalArgumentException(String.format( + "Provided RoleId/SecretId setup not supported. RoleId: %s, SecretId: %s", + roleId, secretId)); + } + + private static Node unwrapResponse(VaultToken token) { + + return AuthenticationSteps.fromHttpRequest( + get("cubbyhole/response").with(createHttpHeaders(token)).as( + VaultResponse.class)).map( + vaultResponse -> { + + Map data = vaultResponse.getRequiredData(); + return VaultResponses.unwrap((String) data.get("response"), + VaultResponse.class); + }); } @Override @@ -139,10 +211,8 @@ public class AppRoleAuthentication implements ClientAuthentication, private VaultToken createTokenUsingAppRole() { - String roleId = getRoleId(); - String secretId = getSecretId(); - - Map login = getAppRoleLogin(roleId, secretId); + Map login = getAppRoleLoginBody(options.getRoleId(), + options.getSecretId()); try { VaultResponse response = restOperations.postForObject("auth/{mount}/login", @@ -161,16 +231,23 @@ public class AppRoleAuthentication implements ClientAuthentication, } } - private String getRoleId() { + private String getRoleId(RoleId roleId) { - if (roleIdPullRequired(options)) { + if (roleId instanceof Provided) { + return ((Provided) roleId).getValue(); + } + + if (roleId instanceof Pull) { + + VaultToken token = ((Pull) roleId).getInitialToken(); try { - ResponseEntity response = restOperations.exchange( + + ResponseEntity entity = restOperations.exchange( "auth/{mount}/role/{role}/role-id", HttpMethod.GET, - createHttpEntity(options.getInitialToken()), VaultResponse.class, - options.getPath(), options.getAppRole()); - return (String) response.getBody().getRequiredData().get("role_id"); + createHttpEntity(token), VaultResponse.class, options.getPath(), + options.getAppRole()); + return (String) entity.getBody().getRequiredData().get("role_id"); } catch (HttpStatusCodeException e) { throw new VaultException(String.format( @@ -179,66 +256,107 @@ public class AppRoleAuthentication implements ClientAuthentication, } } - return options.getRoleId(); - } + if (roleId instanceof Wrapped) { - private static boolean roleIdPullRequired(AppRoleAuthenticationOptions options) { - return options.getRoleId() == null; - } + VaultToken token = ((Wrapped) roleId).getInitialToken(); - private String getSecretId() { + try { - if (secretIdPullRequired(options)) { - // 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()))); - } + ResponseEntity entity = restOperations.exchange( + "cubbyhole/response", HttpMethod.GET, createHttpEntity(token), + VaultResponse.class); + + Map data = entity.getBody().getRequiredData(); + VaultResponse response = VaultResponses.unwrap( + (String) data.get("response"), VaultResponse.class); + + return (String) response.getRequiredData().get("role_id"); } - // 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()) - )); - } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format( + "Cannot unwrap Role id using AppRole: %s", + VaultResponses.getError(e.getResponseBodyAsString()))); } } - return options.getSecretId(); + throw new IllegalArgumentException("Unknown RoleId configuration: " + roleId); } - private static boolean secretIdPullRequired(AppRoleAuthenticationOptions options) { - return options.getSecretId() == null && (options.getInitialToken() != null || options.getUnwrappingToken() != null); + private String getSecretId(SecretId secretId) { + + if (secretId instanceof Provided) { + return ((Provided) secretId).getValue(); + } + + if (secretId instanceof Pull) { + + VaultToken token = ((Pull) secretId).getInitialToken(); + + try { + VaultResponse response = restOperations.postForObject( + "auth/{mount}/role/{role}/secret-id", createHttpEntity(token), + 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()))); + } + } + + if (secretId instanceof Wrapped) { + + VaultToken token = ((Wrapped) secretId).getInitialToken(); + + try { + + ResponseEntity entity = restOperations.exchange( + "cubbyhole/response", HttpMethod.GET, createHttpEntity(token), + VaultResponse.class); + + Map data = entity.getBody().getRequiredData(); + VaultResponse response = VaultResponses.unwrap( + (String) data.get("response"), VaultResponse.class); + + return (String) response.getRequiredData().get("secret_id"); + } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format( + "Cannot unwrap Role id using AppRole: %s", + VaultResponses.getError(e.getResponseBodyAsString()))); + } + } + + throw new IllegalArgumentException("Unknown SecretId configuration: " + secretId); } - private static HttpEntity createHttpEntity(VaultToken token) { + private static HttpHeaders createHttpHeaders(VaultToken token) { HttpHeaders headers = new HttpHeaders(); headers.set("X-Vault-Token", token.getToken()); - return new HttpEntity(null, headers); + + return headers; } - private static Map getAppRoleLogin(String roleId, + private static HttpEntity createHttpEntity(VaultToken token) { + return new HttpEntity(null, createHttpHeaders(token)); + } + + private Map getAppRoleLoginBody(RoleId roleId, SecretId secretId) { + + Map login = new HashMap<>(); + + login.put("role_id", getRoleId(roleId)); + + if (!ClassUtils.isAssignableValue(AbsentSecretId.class, secretId)) { + login.put("secret_id", getSecretId(secretId)); + } + + return login; + } + + private static Map getAppRoleLoginBody(String roleId, @Nullable String secretId) { Map login = new HashMap<>(); 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 4b66f2e3..5a829152 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,7 +17,9 @@ package org.springframework.vault.authentication; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; +import org.springframework.vault.authentication.AppRoleTokens.AbsentSecretId; +import org.springframework.vault.authentication.AppRoleTokens.Provided; +import org.springframework.vault.authentication.AppRoleTokens.Pull; import org.springframework.vault.support.VaultToken; /** @@ -45,14 +47,12 @@ public class AppRoleAuthenticationOptions { /** * The RoleId. */ - @Nullable - private final String roleId; + private final RoleId roleId; /** * The Bind SecretId. */ - @Nullable - private final String secretId; + private final SecretId secretId; /** * Role name used to get roleId and secretID @@ -62,26 +62,21 @@ public class AppRoleAuthenticationOptions { /** * Token associated for pull mode (retrieval of secretId/roleId). + * @deprecated since 2.0, use {@link RoleId#pull(VaultToken)}/ + * {@link SecretId#pull(VaultToken)} to configure pull mode for roleId/secretId. */ @Nullable + @Deprecated 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 unwrappingToken) { + private AppRoleAuthenticationOptions(String path, RoleId roleId, SecretId secretId, + @Nullable String appRole, @Nullable VaultToken initialToken) { this.path = path; this.roleId = roleId; this.secretId = secretId; this.appRole = appRole; this.initialToken = initialToken; - this.unwrappingToken = unwrappingToken; } /** @@ -101,16 +96,14 @@ public class AppRoleAuthenticationOptions { /** * @return the RoleId. */ - @Nullable - public String getRoleId() { + public RoleId getRoleId() { return roleId; } /** * @return the bound SecretId. */ - @Nullable - public String getSecretId() { + public SecretId getSecretId() { return secretId; } @@ -126,21 +119,15 @@ public class AppRoleAuthenticationOptions { /** * @return the initial token for roleId/secretId retrieval in pull mode. * @since 1.1 + * @deprecated since 2.0, use {@link #getRoleId()}/{@link #getSecretId()} to obtain + * configuration modes (pull/wrapped) for an AppRole token. */ @Nullable + @Deprecated public VaultToken getInitialToken() { return initialToken; } - /** - * @return the token used to unwrap the roleId response. - * @since 2.0 - */ - @Nullable - public VaultToken getUnwrappingToken() { - return unwrappingToken; - } - /** * Builder for {@link AppRoleAuthenticationOptions}. */ @@ -149,17 +136,24 @@ public class AppRoleAuthenticationOptions { private String path = DEFAULT_APPROLE_AUTHENTICATION_PATH; @Nullable - private String roleId; + private String providedRoleId; @Nullable - private String secretId; + private RoleId roleId; + @Nullable + private String providedSecretId; + + @Nullable + private SecretId secretId; + + @Nullable private String appRole; + @Nullable + @Deprecated private VaultToken initialToken; - private VaultToken unwrappingToken; - AppRoleAuthenticationOptionsBuilder() { } @@ -183,12 +177,29 @@ public class AppRoleAuthenticationOptions { * * @param roleId must not be empty or {@literal null}. * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + * @since 2.0 */ + public AppRoleAuthenticationOptionsBuilder roleId(RoleId roleId) { + + Assert.notNull(roleId, "RoleId must not be null"); + + this.roleId = roleId; + return this; + } + + /** + * Configure the RoleId. + * + * @param roleId must not be empty or {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + * @deprecated since 2.0, use {@link #roleId(RoleId)}. + */ + @Deprecated public AppRoleAuthenticationOptionsBuilder roleId(String roleId) { Assert.hasText(roleId, "RoleId must not be empty"); - this.roleId = roleId; + this.providedRoleId = roleId; return this; } @@ -197,12 +208,29 @@ public class AppRoleAuthenticationOptions { * * @param secretId must not be empty or {@literal null}. * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + * @since 2.0 */ + public AppRoleAuthenticationOptionsBuilder secretId(SecretId secretId) { + + Assert.notNull(secretId, "SecretId must not be null"); + + this.secretId = secretId; + return this; + } + + /** + * Configure a {@code secretId}. + * + * @param secretId must not be empty or {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. + * @deprecated since 2.0, use {@link #secretId(SecretId)}. + */ + @Deprecated public AppRoleAuthenticationOptionsBuilder secretId(String secretId) { Assert.hasText(secretId, "SecretId must not be empty"); - this.secretId = secretId; + this.providedSecretId = secretId; return this; } @@ -227,7 +255,10 @@ public class AppRoleAuthenticationOptions { * @param initialToken must not be empty or {@literal null}. * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder}. * @since 1.1 + * @deprecated since 2.0, use {@link #roleId(RoleId)}/{@link #secretId(SecretId)} + * to configure pull mode. */ + @Deprecated public AppRoleAuthenticationOptionsBuilder initialToken(VaultToken initialToken) { Assert.notNull(initialToken, "InitialToken must not be null"); @@ -236,21 +267,6 @@ 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 @@ -262,26 +278,158 @@ public class AppRoleAuthenticationOptions { Assert.hasText(path, "Path must not be empty"); - // Role ID is required in order to use push mode (no appRole and initialToken) + if (secretId == null) { - if (StringUtils.isEmpty(roleId) && StringUtils.isEmpty(appRole) - && initialToken == null) { - throw new IllegalArgumentException( - "Either roleId (push mode) or appRole/initialToken (pull mode) must be configured for AppRole authentication"); + if (providedSecretId != null) { + secretId(SecretId.provided(providedSecretId)); + } + else if (initialToken != null) { + secretId(SecretId.pull(initialToken)); + } + else { + secretId(SecretId.absent()); + } } - // AppRole and InitialToken are required in order to use pull mode (no roleId) - if (StringUtils.isEmpty(roleId)) { + if (roleId == null) { + if (providedRoleId != null) { + roleId(RoleId.provided(providedRoleId)); + } + else { + + Assert.notNull( + initialToken, + "AppRole authentication configured for pull mode. InitialToken must not be null (pull mode)"); + roleId(RoleId.pull(initialToken)); + } + } + + if (roleId instanceof Pull || secretId instanceof Pull) { Assert.notNull(appRole, "AppRole authentication configured for pull mode. AppRole must not be null."); - Assert.notNull( - initialToken, - "AppRole authentication configured for pull mode. InitialToken must not be null (pull mode)"); } return new AppRoleAuthenticationOptions(path, roleId, secretId, appRole, - initialToken, unwrappingToken); + initialToken); + } + } + + /** + * RoleId type encapsulating how the roleId is actually obtained. Provides factory + * methods to obtain a {@link RoleId} by wrapping, pull-mode or whether to use a + * string literal. + * + * @since 2.0 + */ + interface RoleId { + + /** + * Create a {@link RoleId} object that obtains its value from unwrapping a + * response using the {@link VaultToken initial token} from a Cubbyhole. + * + * @param initialToken must not be {@literal null}. + * @return {@link RoleId} object that obtains its value from unwrapping a response + * using the {@link VaultToken initial token}. + * @see org.springframework.vault.client.VaultResponses#unwrap(String, Class) + */ + static RoleId wrapped(VaultToken initialToken) { + + Assert.notNull(initialToken, "Initial token must not be null"); + + return new AppRoleTokens.Wrapped(initialToken); + } + + /** + * Create a {@link RoleId} that obtains its value using pull-mode, specifying a + * {@link VaultToken initial token}. The token policy must allow reading the + * roleId from {@code auth/approle/role/(role-name)/role-id}. + * + * @param initialToken must not be {@literal null}. + * @return {@link RoleId} that obtains its value using pull-mode. + */ + static RoleId pull(VaultToken initialToken) { + + Assert.notNull(initialToken, "Initial token must not be null"); + + return new AppRoleTokens.Pull(initialToken); + } + + /** + * Create a {@link RoleId} that encapsulates a static {@code roleId}. + * + * @param roleId must not be {@literal null} or empty. + * @return {@link RoleId} that encapsulates a static {@code roleId}. + */ + static RoleId provided(String roleId) { + + Assert.hasText(roleId, "RoleId must not be null or empty"); + + return new Provided(roleId); + } + } + + /** + * SecretId type encapsulating how the secretId is actually obtained. Provides factory + * methods to obtain a {@link SecretId} by wrapping, pull-mode or whether to use a + * string literal. + * + * @since 2.0 + */ + interface SecretId { + + /** + * Create a {@link SecretId} object that obtains its value from unwrapping a + * response using the {@link VaultToken initial token} from a Cubbyhole. + * + * @param initialToken must not be {@literal null}. + * @return {@link SecretId} object that obtains its value from unwrapping a + * response using the {@link VaultToken initial token}. + * @see org.springframework.vault.client.VaultResponses#unwrap(String, Class) + */ + static SecretId wrapped(VaultToken initialToken) { + + Assert.notNull(initialToken, "Initial token must not be null"); + + return new AppRoleTokens.Wrapped(initialToken); + } + + /** + * Create a {@link SecretId} that obtains its value using pull-mode, specifying a + * {@link VaultToken initial token}. The token policy must allow reading the + * SecretId from {@code auth/approle/role/(role-name)/secret-id}. + * + * @param initialToken must not be {@literal null}. + * @return {@link SecretId} that obtains its value using pull-mode. + */ + static SecretId pull(VaultToken initialToken) { + + Assert.notNull(initialToken, "Initial token must not be null"); + + return new AppRoleTokens.Pull(initialToken); + } + + /** + * Create a {@link SecretId} that encapsulates a static {@code secretId}. + * + * @param secretId must not be {@literal null} or empty. + * @return {@link SecretId} that encapsulates a static {@code SecretId}. + */ + static SecretId provided(String secretId) { + + Assert.hasText(secretId, "SecretId must not be null or empty"); + + return new Provided(secretId); + } + + /** + * Create a {@link SecretId} that represents an absent secretId. Using this object + * will not send a secretId during AppRole login. + * + * @return a {@link SecretId} that represents an absent secretId + */ + static SecretId absent() { + return AbsentSecretId.INSTANCE; } } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleTokens.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleTokens.java new file mode 100644 index 00000000..f9e8ef27 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AppRoleTokens.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; +import org.springframework.vault.support.VaultToken; + +/** + * Predefined {@link RoleId} token types. + * + * @author Mark Paluch + * @since 2.0 + */ +class AppRoleTokens { + + /** + * Absent secretId. + */ + enum AbsentSecretId implements SecretId { + INSTANCE; + } + + /** + * Wrapped roleId/secretId via Cubbyhole. + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + @Getter + static class Wrapped implements RoleId, SecretId { + + final VaultToken initialToken; + } + + /** + * Pull-mode. + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + @Getter + static class Pull implements RoleId, SecretId { + + final VaultToken initialToken; + } + + /** + * Static, provided roleId/secretId. + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + @Getter + static class Provided implements RoleId, SecretId { + + final String value; + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java new file mode 100644 index 00000000..2699c7ef --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java @@ -0,0 +1,124 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +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.vault.util.Version; + +import static org.junit.Assume.assumeTrue; + +/** + * Integration tests for {@link AppRoleAuthentication}. + * + * @author Mark Paluch + * @author Christophe Tafani-Dereeper + */ +public class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport { + + private static Version SUITABLE_FOR_APP_ROLE_TESTS = Version.parse("0.6.2"); + + @Before + public void before() { + + assumeTrue(prepare().getVersion().isGreaterThanOrEqualTo( + SUITABLE_FOR_APP_ROLE_TESTS)); + + if (!prepare().hasAuth("approle")) { + prepare().mountAuth("approle"); + } + + getVaultOperations().doWithSession(restOperations -> { + + Map withSecretId = new HashMap(); + withSecretId.put("policies", "dummy"); // policy + withSecretId.put("bound_cidr_list", "0.0.0.0/0"); + withSecretId.put("bind_secret_id", "true"); + + restOperations.postForEntity("auth/approle/role/with-secret-id", + withSecretId, Map.class); + + Map noSecretIdRole = new HashMap(); + noSecretIdRole.put("policies", "dummy"); // policy + noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); + noSecretIdRole.put("bind_secret_id", "false"); + + restOperations.postForEntity("auth/approle/role/no-secret-id", + noSecretIdRole, Map.class); + + return null; + }); + } + + protected VaultOperations getVaultOperations() { + return prepare().getVaultOperations(); + } + + protected String getRoleId(String roleName) { + return (String) getVaultOperations() + .read(String.format("auth/approle/role/%s/role-id", roleName)).getData() + .get("role_id"); + } + + protected VaultToken generateWrappedSecretIdResponse() { + + return getVaultOperations().doWithVault( + restOperations -> { + + HttpEntity httpEntity = getWrappingHeaders(); + + VaultResponse response = restOperations.exchange( + "auth/approle/role/with-secret-id/secret-id", HttpMethod.PUT, + httpEntity, VaultResponse.class).getBody(); + + return VaultToken.of(response.getWrapInfo().get("token")); + }); + } + + protected VaultToken generateWrappedRoleIdResponse() { + + return getVaultOperations().doWithVault( + restOperations -> { + + HttpEntity httpEntity = getWrappingHeaders(); + + VaultResponse response = restOperations.exchange( + "auth/approle/role/with-secret-id/role-id", HttpMethod.GET, + httpEntity, VaultResponse.class).getBody(); + + return VaultToken.of(response.getWrapInfo().get("token")); + }); + } + + private HttpEntity getWrappingHeaders() { + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Vault-Wrap-Ttl", "3600"); + headers.set("X-Vault-Token", Settings.token().getToken()); + return new HttpEntity<>(null, headers); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java index 3d8b9141..22cf709a 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTests.java @@ -16,32 +16,17 @@ package org.springframework.vault.authentication; import java.util.Collections; -import java.util.HashMap; -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.authentication.AppRoleAuthenticationOptions.RoleId; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; 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; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.junit.Assume.assumeThat; /** * Integration tests for {@link AppRoleAuthentication}. @@ -49,41 +34,8 @@ import static org.junit.Assume.assumeThat; * @author Mark Paluch * @author Christophe Tafani-Dereeper */ -public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSupport { - - @Before - public void before() { - - assumeThat( - prepare().getVaultOperations().opsForSys().health().getVersion(), - not(anyOf(nullValue(), equalTo(""), containsString("0.5"), - containsString("0.6.1")))); - - if (!prepare().hasAuth("approle")) { - prepare().mountAuth("approle"); - } - - getVaultOperations().doWithSession(restOperations -> { - - Map withSecretId = new HashMap(); - withSecretId.put("policies", "dummy"); // policy - withSecretId.put("bound_cidr_list", "0.0.0.0/0"); - withSecretId.put("bind_secret_id", "true"); - - restOperations.postForEntity("auth/approle/role/with-secret-id", - withSecretId, Map.class); - - Map noSecretIdRole = new HashMap(); - noSecretIdRole.put("policies", "dummy"); // policy - noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); - noSecretIdRole.put("bind_secret_id", "false"); - - restOperations.postForEntity("auth/approle/role/no-secret-id", - noSecretIdRole, Map.class); - - return null; - }); - } +public class AppRoleAuthenticationIntegrationTests extends + AppRoleAuthenticationIntegrationTestBase { @Test public void shouldAuthenticateWithRoleIdOnly() { @@ -138,14 +90,29 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor @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(); + + String roleId = getRoleId("with-secret-id"); + VaultToken unwrappingToken = generateWrappedSecretIdResponse(); AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() - .unwrappingToken(VaultToken.of(unwrappingToken)) - .roleId(roleId) - .build(); + .secretId(SecretId.wrapped(unwrappingToken)) + .roleId(RoleId.provided(roleId)).build(); + + AppRoleAuthentication authentication = new AppRoleAuthentication(options, + prepare().getRestTemplate()); + + assertThat(authentication.login()).isNotNull(); + } + + @Test + public void shouldAuthenticateWithWrappedRoleIdAndSecretId() { + + VaultToken secretIdToken = generateWrappedSecretIdResponse(); + VaultToken roleIdToken = generateWrappedRoleIdResponse(); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .secretId(SecretId.wrapped(secretIdToken)) + .roleId(RoleId.wrapped(roleIdToken)).build(); AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getRestTemplate()); @@ -155,13 +122,13 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor @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(); + .secretId(SecretId.wrapped(VaultToken.of(unwrappingToken))) + .roleId(roleId).build(); AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getRestTemplate()); @@ -169,23 +136,6 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor 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() { @@ -233,89 +183,4 @@ public class AppRoleAuthenticationIntegrationTests extends IntegrationTestSuppor "auth/approle/role/with-secret-id/secret-id-accessor/destroy", customSecretIdResponse.getData()); } - - @Test(expected = VaultException.class) - public void authenticationStepsShouldAuthenticatePullModeFailsWithWrongSecretId() { - - String roleId = getRoleId("with-secret-id"); - - AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() - .roleId(roleId).secretId("this-is-a-wrong-secret-id").build(); - - AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( - AppRoleAuthentication.createAuthenticationSteps(options), prepare() - .getRestTemplate()); - - assertThat(executor.login()).isNotNull(); - } - - @Test - public void authenticationStepsShouldAuthenticatePushModeWithProvidedSecretId() { - - String roleId = getRoleId("with-secret-id"); - String secretId = "hello_world_two"; - - VaultResponse customSecretIdResponse = getVaultOperations().write( - "auth/approle/role/with-secret-id/custom-secret-id", - Collections.singletonMap("secret_id", secretId)); - - AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() - .roleId(roleId).secretId(secretId).build(); - - AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( - AppRoleAuthentication.createAuthenticationSteps(options), prepare() - .getRestTemplate()); - - assertThat(executor.login()).isNotNull(); - - getVaultOperations().write( - "auth/approle/role/with-secret-id/secret-id-accessor/destroy", - customSecretIdResponse.getData()); - } - - @Test - public void authenticationStepsShouldAuthenticatePushMode() { - - String roleId = getRoleId("with-secret-id"); - - AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() - .roleId(roleId).appRole("with-secret-id").initialToken(Settings.token()) - .build(); - - AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( - AppRoleAuthentication.createAuthenticationSteps(options), prepare() - .getRestTemplate()); - - assertThat(executor.login()).isNotNull(); - } - - private VaultOperations getVaultOperations() { - return prepare().getVaultOperations(); - } - - private String getRoleId(String roleName) { - return (String) getVaultOperations() - .read(String.format("auth/approle/role/%s/role-id", roleName)).getData() - .get("role_id"); - } - - @Nullable - private String generateWrappedSecretIdResponse() { - return getVaultOperations().doWithVault(new RestOperationsCallback() { - @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 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"); - } - }); - - } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java new file mode 100644 index 00000000..95e8d1f4 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.vault.authentication; + +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.vault.util.Settings; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link AppRoleAuthentication} using + * {@link AuthenticationStepsExecutor}. + * + * @author Mark Paluch + * @author Christophe Tafani-Dereeper + */ +public class AppRoleAuthenticationStepsIntegrationTests extends + AppRoleAuthenticationIntegrationTestBase { + + @Test + public void authenticationStepsShouldAuthenticateWithWrappedSecretId() { + + String roleId = getRoleId("with-secret-id"); + VaultToken unwrappingToken = generateWrappedSecretIdResponse(); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .secretId(SecretId.wrapped(unwrappingToken)).roleId(roleId).build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + + @Test + public void authenticationStepsShouldAuthenticateWithWrappedRoleId() { + + String secretId = (String) getVaultOperations() + .write(String.format("auth/approle/role/%s/secret-id", "with-secret-id"), + null).getData().get("secret_id"); + + VaultToken roleIdToken = generateWrappedRoleIdResponse(); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .secretId(SecretId.provided(secretId)) + .roleId(RoleId.wrapped(roleIdToken)).build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + + @Test + public void authenticationStepsShouldAuthenticateWithPullSecretId() { + + String roleId = getRoleId("with-secret-id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .appRole("with-secret-id").secretId(SecretId.pull(Settings.token())) + .roleId(roleId).build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + + @Test + public void authenticationStepsShouldAuthenticateWithPullRoleId() { + + String secretId = (String) getVaultOperations() + .write(String.format("auth/approle/role/%s/secret-id", "with-secret-id"), + null).getData().get("secret_id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .secretId(SecretId.provided(secretId)).appRole("with-secret-id") + .roleId(RoleId.pull(Settings.token())).build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + + @Test(expected = VaultException.class) + public void authenticationStepsShouldAuthenticatePullModeFailsWithWrongSecretId() { + + String roleId = getRoleId("with-secret-id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(roleId).secretId("this-is-a-wrong-secret-id").build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } + + @Test + public void authenticationStepsShouldAuthenticatePushModeWithProvidedSecretId() { + + String roleId = getRoleId("with-secret-id"); + String secretId = "hello_world_two"; + + VaultResponse customSecretIdResponse = getVaultOperations().write( + "auth/approle/role/with-secret-id/custom-secret-id", + Collections.singletonMap("secret_id", secretId)); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(roleId).secretId(secretId).build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + + getVaultOperations().write( + "auth/approle/role/with-secret-id/secret-id-accessor/destroy", + customSecretIdResponse.getData()); + } + + @Test + public void authenticationStepsShouldAuthenticatePushMode() { + + String roleId = getRoleId("with-secret-id"); + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(roleId).appRole("with-secret-id").initialToken(Settings.token()) + .build(); + + AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( + AppRoleAuthentication.createAuthenticationSteps(options), prepare() + .getRestTemplate()); + + assertThat(executor.login()).isNotNull(); + } +} 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 6e5e3044..dcfdbe71 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 @@ -17,6 +17,7 @@ package org.springframework.vault.authentication; import java.time.Duration; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; @@ -24,6 +25,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; import org.springframework.vault.client.VaultClients; import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler; import org.springframework.vault.support.VaultToken; @@ -46,6 +48,8 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat */ public class AppRoleAuthenticationUnitTests { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private RestTemplate restTemplate; private MockRestServiceServer mockRest; @@ -177,30 +181,31 @@ public class AppRoleAuthenticationUnitTests { } @Test - public void loginShouldUnwrapSecretIdResponse() { + public void loginShouldUnwrapSecretIdResponse() throws Exception { AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() - .roleId("my_role_id").unwrappingToken(VaultToken.of("unwrapping_token")) - .build(); + .roleId("my_role_id") + .secretId(SecretId.wrapped(VaultToken.of("unwrapping_token"))).build(); + + String wrappedResponse = "{" + + " \"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" + + "}"; // Expect a first request to unwrap the response - mockRest.expect(requestTo("/sys/wrapping/unwrap")) + mockRest.expect(requestTo("/cubbyhole/response")) .andExpect(header("X-Vault-Token", "unwrapping_token")) - .andExpect(method(HttpMethod.POST)) + .andExpect(method(HttpMethod.GET)) .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" - + "}")); + withSuccess().contentType(MediaType.APPLICATION_JSON).body( + "{\"data\":{\"response\":" + + OBJECT_MAPPER + .writeValueAsString(wrappedResponse) + + "} }")); // Also expect a second request to retrieve a token mockRest.expect(requestTo("/auth/approle/login")) diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 6960c799..b5a0fef8 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -10,6 +10,7 @@ * Transit batch encrypt and decrypt support. * Policy management for policies stored as JSON. * Support CSR signing, certificate revocation and CRL retrieval. +* RoleId/SecretId unwrapping for <>. [[new-features.1-1-0]] === What's new in Spring Vault 1.1.0 diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc index 80129900..e5897389 100644 --- a/src/main/asciidoc/reference/authentication.adoc +++ b/src/main/asciidoc/reference/authentication.adoc @@ -197,7 +197,7 @@ AppRole authentication consists of two hard to guess (secret) tokens: RoleId and Spring Vault supports AppRole authentication by providing either RoleId only or together with a provided SecretId and fetching RoleId/SecretId from Vault -(push and pull modes). +(push and pull modes with response unwrapping). ==== [source,java] @@ -211,8 +211,8 @@ class AppConfig extends AbstractVaultConfiguration { public ClientAuthentication clientAuthentication() { AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() - .roleId("…") - .secretId("…") + .roleId(RoleId.provided("…")) + .secretId(SecretId.wrapped(VaultToken.of("…"))) .build(); return new AppRoleAuthentication(options, restOperations()); @@ -238,9 +238,11 @@ class AppConfig extends AbstractVaultConfiguration { @Override public ClientAuthentication clientAuthentication() { + VaultToken initialToken = VaultToken.of("…"); AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .appRole("…") - .initialToken(VaultToken.of("…")) + .roleId(RoleId.pull(initialToken)) + .secretId(SecretId.pull(initialToken)) .build(); return new AppRoleAuthentication(options, restOperations());