diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java index 3771d854..dbe7d3c1 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LifecycleAwareSessionManager.java @@ -44,15 +44,15 @@ import org.springframework.web.client.RestOperations; *

* This {@link SessionManager} also implements {@link DisposableBean} to revoke the * {@link LoginToken} once it's not required anymore. Token revocation will stop regular - * token refresh. Tokens are only revoked only if the associated - * {@link ClientAuthentication} returns a {@link LoginToken}. + * token refresh. Tokens are only revoked if the associated {@link ClientAuthentication} + * returns a {@link LoginToken#isServiceToken() service token}. *

* If Token renewal runs into a client-side error, it assumes the token was * revoked/expired. It discards the token state so the next attempt will lead to another * login attempt. *

- * By default, {@link VaultToken} are looked up in Vault to determine renewability and the - * remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}. + * By default, {@link VaultToken} are looked up in Vault to determine renewability, + * remaining TTL, accessor and type, see {@link #setTokenSelfLookupEnabled(boolean)}. *

* The session manager dispatches authentication events to {@link AuthenticationListener} * and {@link AuthenticationErrorListener}. Event notifications are dispatched either on @@ -390,7 +390,12 @@ public class LifecycleAwareSessionManager extends LifecycleAwareSessionManagerSu } public boolean isRevocable() { - return this.revocable; + + if (token instanceof LoginToken login && login.isServiceToken()) { + return this.revocable; + } + + return false; } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginToken.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginToken.java index 0f0d2f58..a9da638b 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginToken.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginToken.java @@ -16,7 +16,9 @@ package org.springframework.vault.authentication; import java.time.Duration; +import java.util.Arrays; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.vault.support.VaultToken; @@ -34,12 +36,29 @@ public class LoginToken extends VaultToken { */ private final Duration leaseDuration; - private LoginToken(char[] token, Duration duration, boolean renewable) { + @Nullable + private final String accessor; + + @Nullable + private final String type; + + private LoginToken(char[] token, Duration duration, boolean renewable, @Nullable String accessor, + @Nullable String type) { super(token); this.leaseDuration = duration; this.renewable = renewable; + this.accessor = accessor; + this.type = type; + } + + /** + * @return a new {@link LoginTokenBuilder}. + * @since 3.0.2 + */ + public static LoginTokenBuilder builder() { + return new LoginTokenBuilder(); } /** @@ -79,7 +98,7 @@ public class LoginToken extends VaultToken { Assert.notNull(leaseDuration, "Lease duration must not be null"); Assert.isTrue(!leaseDuration.isNegative(), "Lease duration must not be negative"); - return new LoginToken(token, leaseDuration, false); + return new LoginToken(token, leaseDuration, false, null, null); } /** @@ -110,7 +129,7 @@ public class LoginToken extends VaultToken { Assert.notNull(leaseDuration, "Lease duration must not be null"); Assert.isTrue(!leaseDuration.isNegative(), "Lease duration must not be negative"); - return new LoginToken(token, leaseDuration, true); + return new LoginToken(token, leaseDuration, true, null, null); } /** @@ -127,14 +146,169 @@ public class LoginToken extends VaultToken { return this.renewable; } + /** + * @return the token accessor. + * @since 3.0.2 + */ + @Nullable + public String getAccessor() { + return accessor; + } + + /** + * @return the token type. + * @since 3.0.2 + * @see #isBatchToken() + * @see #isServiceToken()) + */ + @Nullable + public String getType() { + return type; + } + + /** + * @return {@literal true} if the token is a batch token. + * @since 3.0.2 + */ + public boolean isBatchToken() { + return "batch".equals(this.type); + } + + /** + * @return {@literal true} if the token is a service token. + * @since 3.0.2 + */ + public boolean isServiceToken() { + return this.type == null || "service".equals(this.type); + } + @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append(getClass().getSimpleName()); sb.append(" [renewable=").append(this.renewable); sb.append(", leaseDuration=").append(this.leaseDuration); + sb.append(", type=").append(this.type); sb.append(']'); return sb.toString(); } + /** + * Builder for {@link LoginToken}. + * + * @since 3.0.2 + */ + public static class LoginTokenBuilder { + + @Nullable + private char[] token; + + private boolean renewable; + + /** + * Duration in seconds. + */ + private Duration leaseDuration = Duration.ZERO; + + @Nullable + private String accessor; + + @Nullable + private String type; + + private LoginTokenBuilder() { + } + + /** + * Configure the token value. This is a required builder property. Without this + * property, you cannot {@link #build()} a {@link LoginToken}. + * @param token must not be empty or {@literal null}. + * @return {@code this} {@link LoginTokenBuilder}. + */ + public LoginTokenBuilder token(String token) { + + Assert.hasText(token, "Token must not be empty"); + + return token(token.toCharArray()); + } + + /** + * Configure the token value. This is a required builder property. Without this + * property, you cannot {@link #build()} a {@link LoginToken}. + * @param token must not be empty or {@literal null}. + * @return {@code this} {@link LoginTokenBuilder}. + */ + public LoginTokenBuilder token(char[] token) { + + Assert.notNull(token, "Token must not be null"); + Assert.isTrue(token.length > 0, "Token must not be empty"); + + this.token = token; + return this; + } + + /** + * Configure whether the token is renewable. + * @param renewable + * @return {@code this} {@link LoginTokenBuilder}. + */ + public LoginTokenBuilder renewable(boolean renewable) { + + this.renewable = renewable; + return this; + } + + /** + * Configure the lease duration. + * @param leaseDuration must not be {@literal null}. + * @return {@code this} {@link LoginTokenBuilder}. + */ + public LoginTokenBuilder leaseDuration(Duration leaseDuration) { + + Assert.notNull(leaseDuration, "Lease duration must not be empty"); + + this.leaseDuration = leaseDuration; + return this; + } + + /** + * Configure the token accessor. + * @param accessor must not be empty or {@literal null}. + * @return {@code this} {@link LoginTokenBuilder}. + */ + public LoginTokenBuilder accessor(String accessor) { + + Assert.hasText(accessor, "Token accessor must not be empty"); + + this.accessor = accessor; + return this; + } + + /** + * Configure the token type. + * @param type must not be empty or {@literal null}. + * @return {@code this} {@link LoginTokenBuilder}. + */ + public LoginTokenBuilder type(String type) { + + Assert.hasText(type, "Token type must not be empty"); + + this.type = type; + return this; + } + + /** + * Build a new {@link LoginToken} instance. {@link #token} must be configured. + * @return a new {@link LoginToken} instance. + */ + public LoginToken build() { + + Assert.notNull(token, "Token must not be null"); + + return new LoginToken(Arrays.copyOf(this.token, this.token.length), this.leaseDuration, this.renewable, + this.accessor, this.type); + } + + } + } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenAdapter.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenAdapter.java index b5a30c92..b6733d46 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenAdapter.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenAdapter.java @@ -15,13 +15,11 @@ */ package org.springframework.vault.authentication; -import java.time.Duration; import java.util.Map; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.vault.VaultException; import org.springframework.vault.client.VaultHttpHeaders; @@ -35,7 +33,7 @@ import org.springframework.web.client.RestOperations; /** * Adapts tokens created by a {@link ClientAuthentication} to a {@link LoginToken}. Allows * decoration of a {@link ClientAuthentication} object to perform a self-lookup after - * token retrieval to obtain the remaining TTL and renewability. + * token retrieval to obtain the remaining TTL, renewability, accessor and token type. *

* Using this adapter decrements the usage counter for the created token. * @@ -77,14 +75,7 @@ public class LoginTokenAdapter implements ClientAuthentication { Map data = lookupSelf(restOperations, token); - Boolean renewable = (Boolean) data.get("renewable"); - Number ttl = (Number) data.get("ttl"); - - if (renewable != null && renewable) { - return LoginToken.renewable(token.toCharArray(), getLeaseDuration(ttl)); - } - - return LoginToken.of(token.toCharArray(), getLeaseDuration(ttl)); + return LoginTokenUtil.from(token.toCharArray(), data); } private static Map lookupSelf(RestOperations restOperations, VaultToken token) { @@ -106,8 +97,4 @@ public class LoginTokenAdapter implements ClientAuthentication { } } - static Duration getLeaseDuration(@Nullable Number ttl) { - return ttl == null ? Duration.ZERO : Duration.ofSeconds(ttl.longValue()); - } - } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenUtil.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenUtil.java index b6816541..b9ce76ca 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenUtil.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/LoginTokenUtil.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.Map; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Utility class for {@link LoginToken}. @@ -51,26 +52,43 @@ final class LoginTokenUtil { * @return the {@link LoginToken} * @since 2.0 */ - static LoginToken from(char[] token, Map auth) { + static LoginToken from(char[] token, Map auth) { Assert.notNull(auth, "Authentication must not be null"); Boolean renewable = (Boolean) auth.get("renewable"); Number leaseDuration = (Number) auth.get("lease_duration"); + String accessor = (String) auth.get("accessor"); + String type = (String) auth.get("type"); if (leaseDuration == null) { leaseDuration = (Number) auth.get("ttl"); } - if (renewable != null && renewable) { - return LoginToken.renewable(token, Duration.ofSeconds(leaseDuration.longValue())); + if (type == null) { + type = (String) auth.get("token_type"); + } + + LoginToken.LoginTokenBuilder builder = LoginToken.builder(); + builder.token(token); + + if (StringUtils.hasText(accessor)) { + builder.accessor(accessor); } if (leaseDuration != null) { - return LoginToken.of(token, Duration.ofSeconds(leaseDuration.longValue())); + builder.leaseDuration(Duration.ofSeconds(leaseDuration.longValue())); } - return LoginToken.of(token); + if (renewable != null) { + builder.renewable(renewable); + } + + if (StringUtils.hasText(type)) { + builder.type(type); + } + + return builder.build(); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java index b83c0586..a418157d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManager.java @@ -47,15 +47,15 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti *

* This {@link ReactiveSessionManager} also implements {@link DisposableBean} to revoke * the {@link LoginToken} once it's not required anymore. Token revocation will stop - * regular token refresh. Tokens are only revoked only if the associated - * {@link VaultTokenSupplier} returns a {@link LoginToken}. + * regular token refresh. Tokens are only revoked if the associated + * {@link VaultTokenSupplier} returns a {@link LoginToken#isServiceToken() service token}. *

* If Token renewal runs into a client-side error, it assumes the token was * revoked/expired. It discards the token state so the next attempt will lead to another * login attempt. *

- * By default, {@link VaultToken} are looked up in Vault to determine renewability and the - * remaining TTL, see {@link #setTokenSelfLookupEnabled(boolean)}. + * By default, {@link VaultToken} are looked up in Vault to determine renewability, + * remaining TTL, accessor and type, see {@link #setTokenSelfLookupEnabled(boolean)}. *

* The session manager dispatches authentication events to {@link AuthenticationListener} * and {@link AuthenticationErrorListener}. @@ -371,17 +371,7 @@ public class ReactiveLifecycleAwareSessionManager extends LifecycleAwareSessionM Mono> data = lookupSelf(webClient, token); - return data.map(it -> { - - Boolean renewable = (Boolean) it.get("renewable"); - Number ttl = (Number) it.get("ttl"); - - if (renewable != null && renewable) { - return LoginToken.renewable(token.toCharArray(), LoginTokenAdapter.getLeaseDuration(ttl)); - } - - return LoginToken.of(token.toCharArray(), LoginTokenAdapter.getLeaseDuration(ttl)); - }); + return data.map(it -> LoginTokenUtil.from(token.toCharArray(), it)); } private static Mono> lookupSelf(WebClient webClient, VaultToken token) { @@ -431,7 +421,12 @@ public class ReactiveLifecycleAwareSessionManager extends LifecycleAwareSessionM } public boolean isRevocable() { - return this.revocable; + + if (token instanceof LoginToken login && login.isServiceToken()) { + return this.revocable; + } + + return false; } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultToken.java b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultToken.java index 22f50735..ef6b63e3 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/VaultToken.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/VaultToken.java @@ -71,7 +71,7 @@ public class VaultToken { * @since 1.1 */ public char[] toCharArray() { - return this.token; + return Arrays.copyOf(this.token, this.token.length); } @Override @@ -89,6 +89,7 @@ public class VaultToken { return Arrays.hashCode(this.token); } + @Override public String toString() { return getClass().getSimpleName(); } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java index fb5d8959..142c0a87 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LifecycleAwareSessionManagerUnitTests.java @@ -197,6 +197,22 @@ class LifecycleAwareSessionManagerUnitTests { verifyNoMoreInteractions(this.listener); } + @Test + void shouldNotRevokeBatchToken() { + + LoginToken batchToken = LoginToken.builder().token("login").type("batch").build(); + + when(this.clientAuthentication.login()).thenReturn(batchToken); + + this.sessionManager.setTokenSelfLookupEnabled(false); + this.sessionManager.renewToken(); + this.sessionManager.destroy(); + + verifyNoInteractions(this.restOperations); + verify(this.listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verifyNoMoreInteractions(this.listener); + } + @Test @SuppressWarnings("unchecked") void shouldNotThrowExceptionsOnRevokeErrors() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUnitTests.java index 9568ccf1..4fb33d2c 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUnitTests.java @@ -39,11 +39,41 @@ class LoginTokenUnitTests { @Test void toStringShouldPrintFields() { - assertThat(LoginToken.of("token").toString()).isEqualTo("LoginToken [renewable=false, leaseDuration=PT0S]"); - assertThat(LoginToken.of("token".toCharArray(), Duration.ofSeconds(1)).toString()) - .isEqualTo("LoginToken [renewable=false, leaseDuration=PT1S]"); - assertThat(LoginToken.renewable("token".toCharArray(), Duration.ofSeconds(1)).toString()) - .isEqualTo("LoginToken [renewable=true, leaseDuration=PT1S]"); + assertThat(LoginToken.of("token")).hasToString("LoginToken [renewable=false, leaseDuration=PT0S, type=null]"); + assertThat(LoginToken.of("token".toCharArray(), Duration.ofSeconds(1))) + .hasToString("LoginToken [renewable=false, leaseDuration=PT1S, type=null]"); + assertThat(LoginToken.renewable("token".toCharArray(), Duration.ofSeconds(1))) + .hasToString("LoginToken [renewable=true, leaseDuration=PT1S, type=null]"); + assertThat(LoginToken.builder().token("foo").type("service").build()) + .hasToString("LoginToken [renewable=false, leaseDuration=PT0S, type=service]"); + } + + @Test + void shouldConstructTokenWithAccessor() { + + assertThat(LoginToken.of("token").getAccessor()).isNull(); + + LoginToken loginToken = LoginToken.builder().token("token").accessor("acc").build(); + assertThat(loginToken.getToken()).isEqualTo("token"); + assertThat(loginToken.getAccessor()).isEqualTo("acc"); + } + + @Test + void shouldConstructServiceToken() { + + assertThat(LoginToken.of("token").isServiceToken()).isTrue(); + + LoginToken loginToken = LoginToken.builder().token("token").type("service").build(); + assertThat(loginToken.isServiceToken()).isTrue(); + } + + @Test + void shouldConstructBatchToken() { + + assertThat(LoginToken.of("token").isBatchToken()).isFalse(); + + LoginToken loginToken = LoginToken.builder().token("token").type("batch").build(); + assertThat(loginToken.isBatchToken()).isTrue(); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUtilUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUtilUnitTests.java new file mode 100644 index 00000000..979849c4 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/LoginTokenUtilUnitTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 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 java.io.Serializable; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; + +/** + * Unit tests for {@link LoginTokenUtil}. + * + * @author Mark Paluch + */ +class LoginTokenUtilUnitTests { + + @Test + void shouldCreateServiceToken() { + + Map response = Map.of("type", "service", "ttl", 100, "accessor", + "B6oixijqmeR4bsLOJH88Ska9"); + + LoginToken loginToken = LoginTokenUtil.from("foo".toCharArray(), response); + + assertThat(loginToken.isServiceToken()).isTrue(); + assertThat(loginToken.getAccessor()).isEqualTo("B6oixijqmeR4bsLOJH88Ska9"); + } + + @Test + void shouldCreateBatchToken() { + + Map response = Map.of("type", "batch", "ttl", 100, "accessor", + "B6oixijqmeR4bsLOJH88Ska9"); + + LoginToken loginToken = LoginTokenUtil.from("foo".toCharArray(), response); + + assertThat(loginToken.isBatchToken()).isTrue(); + assertThat(loginToken.getAccessor()).isEqualTo("B6oixijqmeR4bsLOJH88Ska9"); + } + +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java index 0d22df09..5b108d98 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ReactiveLifecycleAwareSessionManagerUnitTests.java @@ -258,6 +258,26 @@ class ReactiveLifecycleAwareSessionManagerUnitTests { verifyNoMoreInteractions(this.listener); } + @Test + void shouldNotRevokeBatchTokenOnDestroy() { + + LoginToken batchToken = LoginToken.builder().token("login").type("batch").build(); + + mockToken(batchToken); + + this.sessionManager.setTokenSelfLookupEnabled(false); + this.sessionManager.renewToken() // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + this.sessionManager.destroy(); + + verify(this.webClient, never()).post(); + verify(this.webClient.post(), never()).uri("auth/token/revoke-self"); + verify(this.listener).onAuthenticationEvent(any(AfterLoginEvent.class)); + verifyNoMoreInteractions(this.listener); + } + @Test void shouldNotThrowExceptionsOnRevokeErrors() {