From 35079a54bbf22c6b159593d2c3e5a2f32d07c46d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 9 Sep 2019 13:16:09 +0200 Subject: [PATCH] Switch to sys/wrapping endpoints for response unwrapping. Cubbyhole and wrapped AppRole authentications now use sys/wrapping endpoints to unwrap responses by default. Both authentication mechanisms can be configured with UnwrappingEndpoints to switch back to cubbyhole. Closes gh-163. --- .../authentication/AppRoleAuthentication.java | 54 ++++++----- .../AppRoleAuthenticationOptions.java | 37 ++++++- .../authentication/AuthenticationSteps.java | 13 +++ .../CubbyholeAuthentication.java | 72 +++++++++----- .../CubbyholeAuthenticationOptions.java | 41 +++++++- .../authentication/UnwrappingEndpoints.java | 97 +++++++++++++++++++ .../vault/core/lease/LeaseEndpoints.java | 2 +- ...RoleAuthenticationIntegrationTestBase.java | 11 +++ ...AppRoleAuthenticationIntegrationTests.java | 9 +- ...leAuthenticationStepsIntegrationTests.java | 9 +- .../AppRoleAuthenticationUnitTests.java | 47 ++++++++- ...holeAuthenticationIntegrationTestBase.java | 14 ++- ...bbyholeAuthenticationIntegrationTests.java | 6 +- ...uthenticationOperatorIntegrationTests.java | 1 + ...leAuthenticationStepsIntegrationTests.java | 1 + .../CubbyholeAuthenticationUnitTests.java | 46 +++++++-- src/main/asciidoc/new-features.adoc | 1 + .../asciidoc/reference/authentication.adoc | 6 +- 18 files changed, 386 insertions(+), 81 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/UnwrappingEndpoints.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 0476625a..ad36becb 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 @@ -43,6 +43,7 @@ import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.get; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.method; import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.post; /** @@ -136,9 +137,10 @@ public class AppRoleAuthentication } if (roleId instanceof Wrapped) { - return unwrapResponse(((Wrapped) roleId).getInitialToken()) - .map(vaultResponse -> (String) vaultResponse.getRequiredData() - .get("role_id")); + return unwrapResponse(options.getUnwrappingEndpoints(), + ((Wrapped) roleId).getInitialToken()) + .map(vaultResponse -> (String) vaultResponse.getRequiredData() + .get("role_id")); } throw new IllegalArgumentException("Unknown RoleId configuration: " + roleId); @@ -164,25 +166,24 @@ public class AppRoleAuthentication if (secretId instanceof Wrapped) { - return unwrapResponse(((Wrapped) secretId).getInitialToken()) - .map(vaultResponse -> (String) vaultResponse.getRequiredData() - .get("secret_id")); + return unwrapResponse(options.getUnwrappingEndpoints(), + ((Wrapped) secretId).getInitialToken()) + .map(vaultResponse -> (String) vaultResponse.getRequiredData() + .get("secret_id")); } throw new IllegalArgumentException("Unknown SecretId configuration: " + secretId); } - private static Node unwrapResponse(VaultToken token) { + private static Node unwrapResponse( + UnwrappingEndpoints unwrappingEndpoints, 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); - }); + return AuthenticationSteps + .fromHttpRequest(method(unwrappingEndpoints.getUnwrapRequestMethod(), + unwrappingEndpoints.getPath()).with(createHttpHeaders(token)) + .as(VaultResponse.class)) + .map(unwrappingEndpoints::unwrap); } @Override @@ -247,14 +248,14 @@ public class AppRoleAuthentication VaultToken token = ((Wrapped) roleId).getInitialToken(); try { - + UnwrappingEndpoints unwrappingEndpoints = options + .getUnwrappingEndpoints(); ResponseEntity entity = restOperations.exchange( - "cubbyhole/response", HttpMethod.GET, createHttpEntity(token), - VaultResponse.class); + unwrappingEndpoints.getPath(), + unwrappingEndpoints.getUnwrapRequestMethod(), + createHttpEntity(token), VaultResponse.class); - Map data = entity.getBody().getRequiredData(); - VaultResponse response = VaultResponses - .unwrap((String) data.get("response"), VaultResponse.class); + VaultResponse response = unwrappingEndpoints.unwrap(entity.getBody()); return (String) response.getRequiredData().get("role_id"); } @@ -299,13 +300,14 @@ public class AppRoleAuthentication try { + UnwrappingEndpoints unwrappingEndpoints = options + .getUnwrappingEndpoints(); ResponseEntity entity = restOperations.exchange( - "cubbyhole/response", HttpMethod.GET, createHttpEntity(token), - VaultResponse.class); + unwrappingEndpoints.getPath(), + unwrappingEndpoints.getUnwrapRequestMethod(), + createHttpEntity(token), VaultResponse.class); - Map data = entity.getBody().getRequiredData(); - VaultResponse response = VaultResponses - .unwrap((String) data.get("response"), VaultResponse.class); + VaultResponse response = unwrappingEndpoints.unwrap(entity.getBody()); return (String) response.getRequiredData().get("secret_id"); } 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 839a5308..fa5bea7d 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 @@ -60,6 +60,11 @@ public class AppRoleAuthenticationOptions { @Nullable private final String appRole; + /** + * Unwrapping endpoint to cater for functionality across various Vault versions. + */ + private final UnwrappingEndpoints unwrappingEndpoints; + /** * Token associated for pull mode (retrieval of secretId/roleId). * @deprecated since 2.0, use {@link RoleId#pull(VaultToken)}/ @@ -70,12 +75,14 @@ public class AppRoleAuthenticationOptions { private final VaultToken initialToken; private AppRoleAuthenticationOptions(String path, RoleId roleId, SecretId secretId, - @Nullable String appRole, @Nullable VaultToken initialToken) { + @Nullable String appRole, UnwrappingEndpoints unwrappingEndpoints, + @Nullable VaultToken initialToken) { this.path = path; this.roleId = roleId; this.secretId = secretId; this.appRole = appRole; + this.unwrappingEndpoints = unwrappingEndpoints; this.initialToken = initialToken; } @@ -116,6 +123,14 @@ public class AppRoleAuthenticationOptions { return appRole; } + /** + * @return the endpoint configuration. + * @since 2.2 + */ + public UnwrappingEndpoints getUnwrappingEndpoints() { + return unwrappingEndpoints; + } + /** * @return the initial token for roleId/secretId retrieval in pull mode. * @since 1.1 @@ -150,6 +165,8 @@ public class AppRoleAuthenticationOptions { @Nullable private String appRole; + private UnwrappingEndpoints unwrappingEndpoints = UnwrappingEndpoints.SysWrapping; + @Nullable @Deprecated private VaultToken initialToken; @@ -249,6 +266,22 @@ public class AppRoleAuthenticationOptions { return this; } + /** + * Configure the {@link UnwrappingEndpoints} to use. + * + * @param endpoints must not be {@literal null}. + * @return {@code this} {@link AppRoleAuthenticationOptionsBuilder} + * @since 2.2 + */ + public AppRoleAuthenticationOptionsBuilder unwrappingEndpoints( + UnwrappingEndpoints endpoints) { + + Assert.notNull(endpoints, "UnwrappingEndpoints must not be empty"); + + this.unwrappingEndpoints = endpoints; + return this; + } + /** * Configure a {@code initialToken}. * @@ -310,7 +343,7 @@ public class AppRoleAuthenticationOptions { } return new AppRoleAuthenticationOptions(path, roleId, secretId, appRole, - initialToken); + unwrappingEndpoints, initialToken); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java index 4640ed46..d237efa3 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/AuthenticationSteps.java @@ -346,6 +346,19 @@ public class AuthenticationSteps { return new HttpRequestBuilder(HttpMethod.POST, uri); } + /** + * Builder entry point to use {@link HttpMethod} for {@code uriTemplate}. + * + * @param uriTemplate must not be {@literal null} or empty. + * @param uriVariables the variables to expand the template. + * @return a new {@link HttpRequestBuilder}. + * @since 2.2 + */ + public static HttpRequestBuilder method(HttpMethod method, String uriTemplate, + String... uriVariables) { + return new HttpRequestBuilder(method, uriTemplate, uriVariables); + } + private HttpRequestBuilder(HttpMethod method, URI uri) { this.method = method; this.uri = uri; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java index 91977b59..7641f0ec 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthentication.java @@ -28,14 +28,12 @@ import org.springframework.util.Assert; import org.springframework.vault.VaultException; import org.springframework.vault.authentication.AuthenticationSteps.HttpRequest; import org.springframework.vault.client.VaultHttpHeaders; -import org.springframework.vault.client.VaultResponses; import org.springframework.vault.support.VaultResponse; -import org.springframework.vault.support.VaultResponseSupport; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestOperations; -import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.get; +import static org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder.method; /** * Cubbyhole {@link ClientAuthentication} implementation. @@ -176,21 +174,26 @@ public class CubbyholeAuthentication Assert.notNull(options, "CubbyholeAuthenticationOptions must not be null"); - HttpRequest initialRequest = get(options.getPath()) // - .with(VaultHttpHeaders.from(options.getInitialToken())) // + String url = getRequestPath(options); + + HttpMethod unwrapMethod = getRequestMethod(options); + HttpEntity requestEntity = getRequestEntity(options); + + HttpRequest initialRequest = method(unwrapMethod, url) // + .with(requestEntity) // .as(VaultResponse.class); return AuthenticationSteps.fromHttpRequest(initialRequest) // - .map(VaultResponseSupport::getData) // - .login(map -> getToken(options, map)); + .login(it -> getToken(options, it, url)); } @Override public VaultToken login() throws VaultException { - Map data = lookupToken(); + String url = getRequestPath(options); + VaultResponse data = lookupToken(url); - VaultToken tokenToUse = getToken(this.options, data); + VaultToken tokenToUse = getToken(this.options, data, url); if (shouldEnhanceTokenWithSelfLookup(tokenToUse)) { @@ -209,18 +212,17 @@ public class CubbyholeAuthentication } @Nullable - private Map lookupToken() { + private VaultResponse lookupToken(String url) { try { - - ResponseEntity entity = restOperations.exchange( - options.getPath(), HttpMethod.GET, - new HttpEntity<>(VaultHttpHeaders.from(options.getInitialToken())), - VaultResponse.class); + HttpMethod unwrapMethod = getRequestMethod(options); + HttpEntity requestEntity = getRequestEntity(options); + ResponseEntity entity = restOperations.exchange(url, + unwrapMethod, requestEntity, VaultResponse.class); Assert.state(entity.getBody() != null, "Auth response must not be null"); - return entity.getBody().getData(); + return entity.getBody(); } catch (RestClientException e) { throw VaultLoginException.create("Cubbyhole", e); @@ -245,21 +247,43 @@ public class CubbyholeAuthentication return true; } + private static HttpEntity getRequestEntity( + CubbyholeAuthenticationOptions options) { + return new HttpEntity<>(VaultHttpHeaders.from(options.getInitialToken())); + } + + private static HttpMethod getRequestMethod(CubbyholeAuthenticationOptions options) { + + if (options.isWrappedToken()) { + return options.getUnwrappingEndpoints().getUnwrapRequestMethod(); + } + + return HttpMethod.GET; + } + + private static String getRequestPath(CubbyholeAuthenticationOptions options) { + + if (options.isWrappedToken()) { + return options.getUnwrappingEndpoints().getPath(); + } + + return options.getPath(); + } + private static VaultToken getToken(CubbyholeAuthenticationOptions options, - @Nullable Map data) { + VaultResponse response, String url) { if (options.isWrappedToken()) { - Assert.state(data != null, "Auth data must not be null"); + VaultResponse responseToUse = options.getUnwrappingEndpoints() + .unwrap(response); - VaultResponse response = VaultResponses.unwrap((String) data.get("response"), - VaultResponse.class); + Assert.state(responseToUse.getAuth() != null, "Auth field must not be null"); - Assert.state(response.getAuth() != null, "Auth field must not be null"); - - return LoginTokenUtil.from(response.getAuth()); + return LoginTokenUtil.from(responseToUse.getAuth()); } + Map data = response.getData(); if (data == null || data.isEmpty()) { throw new VaultLoginException(String.format( "Cannot retrieve Token from Cubbyhole: Response at %s does not contain a token", @@ -273,6 +297,6 @@ public class CubbyholeAuthentication throw new VaultLoginException(String.format( "Cannot retrieve Token from Cubbyhole: Response at %s does not contain an unique token", - options.getPath())); + url)); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java index 6af149cf..3fb225c8 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CubbyholeAuthenticationOptions.java @@ -41,6 +41,11 @@ public class CubbyholeAuthenticationOptions { */ private final String path; + /** + * Unwrapping endpoint to cater for functionality across various Vault versions. + */ + private final UnwrappingEndpoints unwrappingEndpoints; + /** * Indicates whether the Cubbyhole contains a wrapped token. */ @@ -53,12 +58,14 @@ public class CubbyholeAuthenticationOptions { private final boolean selfLookup; private CubbyholeAuthenticationOptions(VaultToken initialToken, String path, - boolean wrappedToken, boolean selfLookup) { + UnwrappingEndpoints unwrappingEndpoints, boolean wrappedToken, + boolean selfLookup) { this.initialToken = initialToken; this.path = path; this.wrappedToken = wrappedToken; this.selfLookup = selfLookup; + this.unwrappingEndpoints = unwrappingEndpoints; } /** @@ -82,6 +89,14 @@ public class CubbyholeAuthenticationOptions { return path; } + /** + * @return the endpoint configuration. + * @since 2.2 + */ + public UnwrappingEndpoints getUnwrappingEndpoints() { + return unwrappingEndpoints; + } + /** * @return {@literal true} indicates that the Cubbyhole response contains a wrapped * token, otherwise {@literal false} to expect a token in the {@literal data} @@ -113,6 +128,8 @@ public class CubbyholeAuthenticationOptions { @Nullable private String path; + private UnwrappingEndpoints endpoints = UnwrappingEndpoints.SysWrapping; + private boolean wrappedToken; private boolean selfLookup = true; @@ -150,6 +167,22 @@ public class CubbyholeAuthenticationOptions { return this; } + /** + * Configure the {@link UnwrappingEndpoints} to use. + * + * @param endpoints must not be {@literal null}. + * @return {@code this} {@link CubbyholeAuthenticationOptionsBuilder} + * @since 2.2 + */ + public CubbyholeAuthenticationOptionsBuilder unwrappingEndpoints( + UnwrappingEndpoints endpoints) { + + Assert.notNull(endpoints, "UnwrappingEndpoints must not be empty"); + + this.endpoints = endpoints; + return this; + } + /** * Configure whether to use wrapped token responses. * @@ -157,7 +190,7 @@ public class CubbyholeAuthenticationOptions { */ public CubbyholeAuthenticationOptionsBuilder wrapped() { - this.path = "cubbyhole/response"; + this.path = ""; this.wrappedToken = true; return this; } @@ -188,8 +221,8 @@ public class CubbyholeAuthenticationOptions { Assert.notNull(initialToken, "Initial Vault Token must not be null"); Assert.notNull(path, "Path must not be null"); - return new CubbyholeAuthenticationOptions(initialToken, path, wrappedToken, - selfLookup); + return new CubbyholeAuthenticationOptions(initialToken, path, endpoints, + wrappedToken, selfLookup); } } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/UnwrappingEndpoints.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/UnwrappingEndpoints.java new file mode 100644 index 00000000..8e45002b --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/UnwrappingEndpoints.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 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 + * + * https://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 org.springframework.http.HttpMethod; +import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.VaultResponse; + +/** + * Version-specific endpoint implementations for response unwrapping. Uses either legacy + * cubbyhole or {@code sys/wrapping} endpoints. + * + * @author Mark Paluch + * @since 2.2 + */ +public enum UnwrappingEndpoints { + + /** + * Legacy cubbyhole endpoints prior to Vault 0.6.2 ({@literal cubbyhole/response}). + */ + Cubbyhole { + + @Override + String getPath() { + return "cubbyhole/response"; + } + + @Override + VaultResponse unwrap(VaultResponse vaultResponse) { + return VaultResponses.unwrap( + (String) vaultResponse.getRequiredData().get("response"), + VaultResponse.class); + } + + @Override + HttpMethod getUnwrapRequestMethod() { + return HttpMethod.GET; + } + }, + + /** + * Sys/wrapping endpoints for Vault 0.6.2 and higher + * ({@literal /sys/wrapping/unwrap}). + */ + SysWrapping { + + @Override + String getPath() { + return "sys/wrapping/unwrap"; + } + + @Override + VaultResponse unwrap(VaultResponse vaultResponse) { + return vaultResponse; + } + + @Override + HttpMethod getUnwrapRequestMethod() { + return HttpMethod.POST; + } + }; + + /** + * Retrieve the path of the unwrapping endpoint. + * + * @return the unwrapping endpoint path. + */ + abstract String getPath(); + + /** + * Unwrap the response data from {@link VaultResponses}. + * + * @param response the raw response entity. + * @return unwrapped {@link VaultResponse}. + */ + abstract VaultResponse unwrap(VaultResponse response); + + /** + * Unwrapping request {@link HttpMethod method}. + * + * @return the unwrapping request {@link HttpMethod method}. + */ + abstract HttpMethod getUnwrapRequestMethod(); +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/LeaseEndpoints.java b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/LeaseEndpoints.java index 18aeb833..502d8a22 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/lease/LeaseEndpoints.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/lease/LeaseEndpoints.java @@ -66,7 +66,7 @@ public enum LeaseEndpoints { }, /** - * Sys/lease endpoints for Vault 0.8 ans higher ({@literal /sys/leases/…}). + * Sys/lease endpoints for Vault 0.8 and higher ({@literal /sys/leases/…}). */ SysLeases { 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 index 0eb26b81..faf87de1 100644 --- 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 @@ -29,6 +29,7 @@ import org.springframework.vault.support.VaultToken; import org.springframework.vault.util.IntegrationTestSupport; import org.springframework.vault.util.RequiresVaultVersion; import org.springframework.vault.util.Settings; +import org.springframework.vault.util.Version; /** * Integration tests for {@link AppRoleAuthentication}. @@ -40,6 +41,7 @@ import org.springframework.vault.util.Settings; class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport { static final String SUITABLE_FOR_APP_ROLE_TESTS = "0.6.2"; + static final Version sysUnwrapSince = Version.parse("0.6.2"); @BeforeEach public void before() { @@ -113,6 +115,15 @@ class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport { }); } + UnwrappingEndpoints getUnwrappingEndpoints() { + return useSysWrapping() ? UnwrappingEndpoints.SysWrapping + : UnwrappingEndpoints.Cubbyhole; + } + + private boolean useSysWrapping() { + return prepare().getVersion().isGreaterThanOrEqualTo(sysUnwrapSince); + } + private HttpEntity getWrappingHeaders() { HttpHeaders headers = new HttpHeaders(); 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 ddd8de8a..d187fceb 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 @@ -101,7 +101,8 @@ class AppRoleAuthenticationIntegrationTests AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .secretId(SecretId.wrapped(unwrappingToken)) - .roleId(RoleId.provided(roleId)).build(); + .roleId(RoleId.provided(roleId)) + .unwrappingEndpoints(getUnwrappingEndpoints()).build(); AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getRestTemplate()); @@ -117,7 +118,8 @@ class AppRoleAuthenticationIntegrationTests AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .secretId(SecretId.wrapped(secretIdToken)) - .roleId(RoleId.wrapped(roleIdToken)).build(); + .roleId(RoleId.wrapped(roleIdToken)) + .unwrappingEndpoints(getUnwrappingEndpoints()).build(); AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getRestTemplate()); @@ -133,7 +135,8 @@ class AppRoleAuthenticationIntegrationTests AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .secretId(SecretId.wrapped(VaultToken.of(unwrappingToken))) - .roleId(RoleId.provided(roleId)).build(); + .roleId(RoleId.provided(roleId)) + .unwrappingEndpoints(getUnwrappingEndpoints()).build(); AppRoleAuthentication authentication = new AppRoleAuthentication(options, prepare().getRestTemplate()); 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 index e487b4b1..d041798b 100644 --- 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 @@ -40,20 +40,21 @@ class AppRoleAuthenticationStepsIntegrationTests extends AppRoleAuthenticationIntegrationTestBase { @Test - void authenticationStepsShouldAuthenticateWithWrappedSecretId() - throws InterruptedException { + void authenticationStepsShouldAuthenticateWithWrappedSecretId() { String roleId = getRoleId("with-secret-id"); VaultToken unwrappingToken = generateWrappedSecretIdResponse(); AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .secretId(SecretId.wrapped(unwrappingToken)) - .roleId(RoleId.provided(roleId)).build(); + .roleId(RoleId.provided(roleId)) + .unwrappingEndpoints(getUnwrappingEndpoints()).build(); AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( AppRoleAuthentication.createAuthenticationSteps(options), prepare().getRestTemplate()); + assertThat(executor.login()).isNotNull(); } @Test @@ -68,7 +69,7 @@ class AppRoleAuthenticationStepsIntegrationTests AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .secretId(SecretId.provided(secretId)).roleId(RoleId.wrapped(roleIdToken)) - .build(); + .unwrappingEndpoints(getUnwrappingEndpoints()).build(); AuthenticationStepsExecutor executor = new AuthenticationStepsExecutor( AppRoleAuthentication.createAuthenticationSteps(options), 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 e29c45fc..153c6750 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 @@ -183,11 +183,12 @@ class AppRoleAuthenticationUnitTests { } @Test - void loginShouldUnwrapSecretIdResponse() throws Exception { + void loginShouldUnwrapCubbyholeSecretIdResponse() throws Exception { AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .roleId(RoleId.provided("my_role_id")) - .secretId(SecretId.wrapped(VaultToken.of("unwrapping_token"))).build(); + .secretId(SecretId.wrapped(VaultToken.of("unwrapping_token"))) + .unwrappingEndpoints(UnwrappingEndpoints.Cubbyhole).build(); String wrappedResponse = "{" + " \"request_id\": \"aad6a19b-a42b-b750-cafb-51087662f53e\"," @@ -226,4 +227,46 @@ class AppRoleAuthenticationUnitTests { .isEqualTo(Duration.ofSeconds(10)); assertThat(((LoginToken) login).isRenewable()).isTrue(); } + + @Test + void loginShouldUnwrapSecretIdResponse() throws Exception { + + AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() + .roleId(RoleId.provided("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")) + .andExpect(header("X-Vault-Token", "unwrapping_token")) + .andExpect(method(HttpMethod.POST)).andRespond(withSuccess() + .contentType(MediaType.APPLICATION_JSON).body(wrappedResponse)); + + // 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(); + } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java index ed473f87..08939422 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTestBase.java @@ -23,6 +23,7 @@ import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.Version; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +34,8 @@ import static org.assertj.core.api.Assertions.assertThat; */ abstract class CubbyholeAuthenticationIntegrationTestBase extends IntegrationTestSupport { + private static final Version sysUnwrapSince = Version.parse("0.6.2"); + Map prepareWrappedToken() { ResponseEntity response = prepare().getVaultOperations() @@ -42,7 +45,7 @@ abstract class CubbyholeAuthenticationIntegrationTestBase extends IntegrationTes headers.add("X-Vault-Wrap-TTL", "10m"); return restOperations.exchange("auth/token/create", HttpMethod.POST, - new HttpEntity(headers), VaultResponse.class); + new HttpEntity<>(headers), VaultResponse.class); }); Map wrapInfo = response.getBody().getWrapInfo(); @@ -51,4 +54,13 @@ abstract class CubbyholeAuthenticationIntegrationTestBase extends IntegrationTes assertThat(wrapInfo).isNotNull(); return wrapInfo; } + + UnwrappingEndpoints getUnwrappingEndpoints() { + return useSysWrapping() ? UnwrappingEndpoints.SysWrapping + : UnwrappingEndpoints.Cubbyhole; + } + + private boolean useSysWrapping() { + return prepare().getVersion().isGreaterThanOrEqualTo(sysUnwrapSince); + } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java index a431eff6..015314a8 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationIntegrationTests.java @@ -44,6 +44,7 @@ class CubbyholeAuthenticationIntegrationTests String initialToken = wrapInfo.get("token"); CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .unwrappingEndpoints(getUnwrappingEndpoints()) .initialToken(VaultToken.of(initialToken)).wrapped().build(); RestTemplate restTemplate = TestRestTemplateFactory .create(Settings.createSslConfiguration()); @@ -58,6 +59,7 @@ class CubbyholeAuthenticationIntegrationTests void loginShouldFail() { CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .unwrappingEndpoints(getUnwrappingEndpoints()) .initialToken(VaultToken.of("Hello")).wrapped().build(); RestTemplate restTemplate = TestRestTemplateFactory @@ -70,9 +72,7 @@ class CubbyholeAuthenticationIntegrationTests fail("Missing VaultException"); } catch (VaultException e) { - assertThat(e).hasMessageContaining("Cannot login using Cubbyhole") - .hasMessageContaining("permission denied"); + assertThat(e).hasMessageContaining("Cannot login using Cubbyhole"); } } - } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java index 319c84d1..0ec401c4 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationOperatorIntegrationTests.java @@ -47,6 +47,7 @@ class CubbyholeAuthenticationOperatorIntegrationTests String initialToken = wrapInfo.get("token"); CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .unwrappingEndpoints(getUnwrappingEndpoints()) .initialToken(VaultToken.of(initialToken)).wrapped().build(); AuthenticationStepsOperator operator = new AuthenticationStepsOperator( diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java index 1d3a39ac..96fa2068 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationStepsIntegrationTests.java @@ -43,6 +43,7 @@ class CubbyholeAuthenticationStepsIntegrationTests String initialToken = wrapInfo.get("token"); CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .unwrappingEndpoints(getUnwrappingEndpoints()) .initialToken(VaultToken.of(initialToken)).wrapped().build(); RestTemplate restTemplate = TestRestTemplateFactory .create(Settings.createSslConfiguration()); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java index a145637e..898b19dd 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/CubbyholeAuthenticationUnitTests.java @@ -62,7 +62,7 @@ class CubbyholeAuthenticationUnitTests { } @Test - void shouldLoginUsingWrappedLogin() throws Exception { + void shouldLoginUsingCubbyholeLogin() throws Exception { String wrappedResponse = "{\"request_id\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\"," + "\"auth\":{\"client_token\":\"5e6332cf-f003-6369-8cba-5bce2330f6cc\"," @@ -78,7 +78,8 @@ class CubbyholeAuthenticationUnitTests { + "} }")); CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() - .initialToken(VaultToken.of("hello")).wrapped().build(); + .initialToken(VaultToken.of("hello")) + .unwrappingEndpoints(UnwrappingEndpoints.Cubbyhole).wrapped().build(); CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, restTemplate); @@ -94,20 +95,49 @@ class CubbyholeAuthenticationUnitTests { } @Test - void shouldLoginUsingWrappedLoginWithSelfLookup() throws Exception { + void shouldLoginUsingWrappedLogin() { + + String wrappedResponse = "{\"request_id\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\"," + + "\"auth\":{\"client_token\":\"5e6332cf-f003-6369-8cba-5bce2330f6cc\"," + + "\"lease_duration\":0," + + "\"accessor\":\"46b6aebb-187f-932a-26d7-4f3d86a68319\"} }"; + + mockRest.expect(requestTo("/sys/wrapping/unwrap")) + .andExpect(method(HttpMethod.POST)) + .andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body(wrappedResponse)); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() + .initialToken(VaultToken.of("hello")) + .unwrappingEndpoints(UnwrappingEndpoints.SysWrapping).wrapped().build(); + + CubbyholeAuthentication authentication = new CubbyholeAuthentication(options, + restTemplate); + + VaultToken login = authentication.login(); + + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("5e6332cf-f003-6369-8cba-5bce2330f6cc"); + + LoginToken loginToken = (LoginToken) login; + assertThat(loginToken.isRenewable()).isFalse(); + assertThat(loginToken.getLeaseDuration()).isEqualTo(Duration.ZERO); + } + + @Test + void shouldLoginUsingWrappedLoginWithSelfLookup() { String wrappedResponse = "{\"request_id\":\"058222ef-9ab9-ff39-f087-9d5bee64e46d\"," + "\"auth\":{\"client_token\":\"5e6332cf-f003-6369-8cba-5bce2330f6cc\"," + "\"lease_duration\":10," + "\"accessor\":\"46b6aebb-187f-932a-26d7-4f3d86a68319\"} }"; - mockRest.expect(requestTo("/cubbyhole/response")) - .andExpect(method(HttpMethod.GET)) + mockRest.expect(requestTo("/sys/wrapping/unwrap")) + .andExpect(method(HttpMethod.POST)) .andExpect(header(VaultHttpHeaders.VAULT_TOKEN, "hello")) .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) - .body("{\"data\":{\"response\":" - + OBJECT_MAPPER.writeValueAsString(wrappedResponse) - + "} }")); + .body(wrappedResponse)); mockRest.expect(requestTo("/auth/token/lookup-self")) .andExpect(method(HttpMethod.GET)) diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 331384f5..9e3e20d5 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -10,6 +10,7 @@ * `LifecycleAwareSessionManager` and `ReactiveLifecycleAwareSessionManager` emit now ``AuthenticationEvent``s. * <>. * Deprecation of `AppIdAuthentication`. Use `AppRoleAuthentication` instead as recommended by HashiCorp Vault. +* `CubbyholeAuthentication` and wrapped `AppRoleAuthentication` now use `sys/wrapping/unwrap` endpoints by default. [[new-features.2-1-0]] === What's new in Spring Vault 2.1 diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc index 6ee8920b..14022015 100644 --- a/src/main/asciidoc/reference/authentication.adoc +++ b/src/main/asciidoc/reference/authentication.adoc @@ -25,7 +25,7 @@ OS access levels. ==== [source, java] ---- -@PropertySource("configuration.properties"), +@PropertySource("configuration.properties") @Configuration public class Config extends AbstractVaultConfiguration { @@ -669,7 +669,7 @@ class AppConfig extends AbstractVaultConfiguration { ==== [source,shell] ---- -$ vault token-create +$ vault token create Key Value --- ----- token f9e30681-d46a-cdaf-aaa0-2ae0a9ad0819 @@ -678,7 +678,7 @@ token_duration 0s token_renewable false token_policies [root] -$ token-create -use-limit=2 -orphan -no-default-policy -policy=none +$ vault token create -use-limit=2 -orphan -no-default-policy -policy=none Key Value --- ----- token 895cb88b-aef4-0e33-ba65-d50007290780