diff --git a/driver/clirr-ignored-differences.xml b/driver/clirr-ignored-differences.xml index a2530156d3..8cc7629950 100644 --- a/driver/clirr-ignored-differences.xml +++ b/driver/clirr-ignored-differences.xml @@ -568,4 +568,33 @@ org.neo4j.driver.AuthTokenAndExpiration expiringAt(long) + + org/neo4j/driver/AuthTokenManager + 7012 + boolean handleSecurityException(org.neo4j.driver.AuthToken, org.neo4j.driver.exceptions.SecurityException) + + + + org/neo4j/driver/AuthTokenManager + 7002 + void onExpired(org.neo4j.driver.AuthToken) + + + + org/neo4j/driver/AuthTokenManagers + 7002 + org.neo4j.driver.AuthTokenManager expirationBased(java.util.function.Supplier) + + + + org/neo4j/driver/AuthTokenManagers + 7002 + org.neo4j.driver.AuthTokenManager expirationBasedAsync(java.util.function.Supplier) + + + + org/neo4j/driver/exceptions/TokenExpiredRetryableException + 8001 + + diff --git a/driver/src/main/java/org/neo4j/driver/AuthToken.java b/driver/src/main/java/org/neo4j/driver/AuthToken.java index 21f18a1f7b..038529e3ba 100644 --- a/driver/src/main/java/org/neo4j/driver/AuthToken.java +++ b/driver/src/main/java/org/neo4j/driver/AuthToken.java @@ -36,14 +36,14 @@ public sealed interface AuthToken permits InternalAuthToken { /** * Returns a new instance of a type holding both the token and its UTC expiration timestamp. *

- * This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the + * This is used by the bearer token implementation of the {@link AuthTokenManager} supplied by the * {@link AuthTokenManagers}. * * @param utcExpirationTimestamp the UTC expiration timestamp * @return a new instance of a type holding both the token and its UTC expiration timestamp * @since 5.8 - * @see AuthTokenManagers#expirationBased(Supplier) - * @see AuthTokenManagers#expirationBasedAsync(Supplier) + * @see AuthTokenManagers#bearer(Supplier) + * @see AuthTokenManagers#bearerAsync(Supplier) */ @Preview(name = "AuthToken rotation and session auth support") default AuthTokenAndExpiration expiringAt(long utcExpirationTimestamp) { diff --git a/driver/src/main/java/org/neo4j/driver/AuthTokenAndExpiration.java b/driver/src/main/java/org/neo4j/driver/AuthTokenAndExpiration.java index ab4fef74df..0fde6bf14e 100644 --- a/driver/src/main/java/org/neo4j/driver/AuthTokenAndExpiration.java +++ b/driver/src/main/java/org/neo4j/driver/AuthTokenAndExpiration.java @@ -26,12 +26,12 @@ * A container used by the expiration based {@link AuthTokenManager} implementation provided by the driver, it contains an * {@link AuthToken} and its UTC expiration timestamp. *

- * This is used by the expiration-based implementation of the {@link AuthTokenManager} supplied by the + * This is used by the bearer token implementation of the {@link AuthTokenManager} supplied by the * {@link AuthTokenManagers}. * * @since 5.8 - * @see AuthTokenManagers#expirationBased(Supplier) - * @see AuthTokenManagers#expirationBasedAsync(Supplier) + * @see AuthTokenManagers#bearer(Supplier) + * @see AuthTokenManagers#bearerAsync(Supplier) */ @Preview(name = "AuthToken rotation and session auth support") public sealed interface AuthTokenAndExpiration permits InternalAuthTokenAndExpiration { diff --git a/driver/src/main/java/org/neo4j/driver/AuthTokenManager.java b/driver/src/main/java/org/neo4j/driver/AuthTokenManager.java index 1607742552..cf8d621185 100644 --- a/driver/src/main/java/org/neo4j/driver/AuthTokenManager.java +++ b/driver/src/main/java/org/neo4j/driver/AuthTokenManager.java @@ -19,6 +19,9 @@ package org.neo4j.driver; import java.util.concurrent.CompletionStage; +import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException; +import org.neo4j.driver.exceptions.SecurityException; +import org.neo4j.driver.exceptions.SecurityRetryableException; import org.neo4j.driver.util.Preview; /** @@ -49,16 +52,27 @@ public interface AuthTokenManager { *

* Failures will surface via the driver API, like {@link Session#beginTransaction()} method and others. * @return a stage for a valid token, must not be {@code null} or complete with {@code null} - * @see org.neo4j.driver.exceptions.AuthTokenManagerExecutionException + * @see AuthTokenManagerExecutionException */ CompletionStage getToken(); /** - * Handles an error notification emitted by the server if the token is expired. + * Handles {@link SecurityException} that is created based on the server's security error response by determining if + * the given error may be resolved upon next {@link AuthTokenManager#getToken()} invokation. *

- * This will be called when driver emits the {@link org.neo4j.driver.exceptions.TokenExpiredRetryableException}. + * If this method returns {@code true}, the driver wraps the original {@link SecurityException} in + * {@link SecurityRetryableException}. The managed transaction API (like + * {@link Session#executeRead(TransactionCallback)}, etc.) automatically retries its unit of work if no other + * condition is violated, while the other query execution APIs surface this error for external handling. + *

+ * If this method returns {@code false}, the original error remains unchanged. + *

+ * This method must not throw exceptions. * - * @param authToken the expired token + * @param authToken the token + * @param exception the security exception + * @return {@code true} if the exception should be marked as retryable or {@code false} if it should remain unchanged + * @since 5.12 */ - void onExpired(AuthToken authToken); + boolean handleSecurityException(AuthToken authToken, SecurityException exception); } diff --git a/driver/src/main/java/org/neo4j/driver/AuthTokenManagers.java b/driver/src/main/java/org/neo4j/driver/AuthTokenManagers.java index e803b0a443..d8e23189e2 100644 --- a/driver/src/main/java/org/neo4j/driver/AuthTokenManagers.java +++ b/driver/src/main/java/org/neo4j/driver/AuthTokenManagers.java @@ -18,11 +18,17 @@ */ package org.neo4j.driver; +import static java.util.Objects.requireNonNull; + import java.time.Clock; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ForkJoinPool; import java.util.function.Supplier; +import org.neo4j.driver.exceptions.AuthenticationException; +import org.neo4j.driver.exceptions.SecurityException; +import org.neo4j.driver.exceptions.TokenExpiredException; import org.neo4j.driver.internal.security.ExpirationBasedAuthTokenManager; import org.neo4j.driver.util.Preview; @@ -36,13 +42,55 @@ public final class AuthTokenManagers { private AuthTokenManagers() {} /** - * Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp. + * Returns an {@link AuthTokenManager} that manages basic {@link AuthToken} instances. + *

+ * The implementation will only use the token supplier when it needs a new token instance, which would happen if + * the server rejects the current token with {@link AuthenticationException} (see + * {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)}). + * The provided supplier and its completion stages must be non-blocking as documented in the + * {@link AuthTokenManager}. + * + * @param newTokenSupplier a new token stage supplier + * @return a new token manager + * @since 5.12 + */ + public static AuthTokenManager basic(Supplier newTokenSupplier) { + requireNonNull(newTokenSupplier, "newTokenSupplier must not be null"); + return basicAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier)); + } + + /** + * Returns an {@link AuthTokenManager} that manages basic {@link AuthToken} instances. + *

+ * The implementation will only use the token supplier when it needs a new token instance, which would happen if + * the server rejects the current token with {@link AuthenticationException} (see + * {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)}). + * The provided supplier and its completion stages must be non-blocking as documented in the + * {@link AuthTokenManager}. + * + * @param newTokenStageSupplier a new token stage supplier + * @return a new token manager + * @since 5.12 + */ + public static AuthTokenManager basicAsync(Supplier> newTokenStageSupplier) { + requireNonNull(newTokenStageSupplier, "newTokenStageSupplier must not be null"); + return new ExpirationBasedAuthTokenManager( + () -> newTokenStageSupplier.get().thenApply(authToken -> authToken.expiringAt(Long.MAX_VALUE)), + Set.of(AuthenticationException.class), + Clock.systemUTC()); + } + + /** + * Returns an {@link AuthTokenManager} that manages bearer {@link AuthToken} instances with UTC expiration + * timestamp. *

* The implementation will only use the token supplier when it needs a new token instance. This includes the * following conditions: *

    *
  1. token's UTC timestamp is expired
  2. - *
  3. server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})
  4. + *
  5. server rejects the current token with either {@link TokenExpiredException} or + * {@link AuthenticationException} (see + * {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)})
  6. *
*

* The supplier will be called by a task running in the {@link ForkJoinPool#commonPool()} as documented in the @@ -50,28 +98,38 @@ private AuthTokenManagers() {} * * @param newTokenSupplier a new token supplier * @return a new token manager + * @since 5.12 */ - public static AuthTokenManager expirationBased(Supplier newTokenSupplier) { - return expirationBasedAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier)); + public static AuthTokenManager bearer(Supplier newTokenSupplier) { + requireNonNull(newTokenSupplier, "newTokenSupplier must not be null"); + return bearerAsync(() -> CompletableFuture.supplyAsync(newTokenSupplier)); } /** - * Returns an {@link AuthTokenManager} that manages {@link AuthToken} instances with UTC expiration timestamp. + * Returns an {@link AuthTokenManager} that manages bearer {@link AuthToken} instances with UTC expiration + * timestamp. *

* The implementation will only use the token supplier when it needs a new token instance. This includes the * following conditions: *

    *
  1. token's UTC timestamp is expired
  2. - *
  3. server rejects the current token (see {@link AuthTokenManager#onExpired(AuthToken)})
  4. + *
  5. server rejects the current token with either {@link TokenExpiredException} or + * {@link AuthenticationException} (see + * {@link AuthTokenManager#handleSecurityException(AuthToken, SecurityException)})
  6. *
*

* The provided supplier and its completion stages must be non-blocking as documented in the {@link AuthTokenManager}. * * @param newTokenStageSupplier a new token stage supplier * @return a new token manager + * @since 5.12 */ - public static AuthTokenManager expirationBasedAsync( + public static AuthTokenManager bearerAsync( Supplier> newTokenStageSupplier) { - return new ExpirationBasedAuthTokenManager(newTokenStageSupplier, Clock.systemUTC()); + requireNonNull(newTokenStageSupplier, "newTokenStageSupplier must not be null"); + return new ExpirationBasedAuthTokenManager( + newTokenStageSupplier, + Set.of(TokenExpiredException.class, AuthenticationException.class), + Clock.systemUTC()); } } diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/RetryableException.java b/driver/src/main/java/org/neo4j/driver/exceptions/RetryableException.java index 604431cebd..6bc09529e7 100644 --- a/driver/src/main/java/org/neo4j/driver/exceptions/RetryableException.java +++ b/driver/src/main/java/org/neo4j/driver/exceptions/RetryableException.java @@ -22,5 +22,6 @@ * A marker interface for retryable exceptions. *

* This indicates whether an operation that resulted in retryable exception is worth retrying. + * @since 5.0 */ public interface RetryableException {} diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/SecurityRetryableException.java b/driver/src/main/java/org/neo4j/driver/exceptions/SecurityRetryableException.java new file mode 100644 index 0000000000..afbd2b0370 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/exceptions/SecurityRetryableException.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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.neo4j.driver.exceptions; + +import java.io.Serial; +import java.util.Objects; +import org.neo4j.driver.AuthToken; +import org.neo4j.driver.util.Experimental; +import org.neo4j.driver.util.Preview; + +/** + * Indicates that the contained {@link SecurityException} is a {@link RetryableException}, which is determined by the + * {@link org.neo4j.driver.AuthTokenManager#handleSecurityException(AuthToken, SecurityException)} method. + *

+ * The original {@link java.lang.SecurityException} is always available as a {@link Throwable#getCause()}. The + * {@link SecurityRetryableException#code()} and {@link SecurityRetryableException#getMessage()} supply the values from + * the original exception. + * + * @since 5.12 + */ +@Preview(name = "AuthToken rotation and session auth support") +public class SecurityRetryableException extends SecurityException implements RetryableException { + @Serial + private static final long serialVersionUID = 3914900631374208080L; + + /** + * The original security exception. + */ + private final SecurityException exception; + + /** + * Creates a new instance. + * + * @param exception the original security exception, must not be {@code null} + */ + public SecurityRetryableException(SecurityException exception) { + super(exception.getMessage(), exception); + this.exception = Objects.requireNonNull(exception); + } + + @Override + public String code() { + return exception.code(); + } + + @Override + public String getMessage() { + return exception.getMessage(); + } + + /** + * Returns the original security exception. + * + * @return the original security exception + */ + @Experimental + public SecurityException securityException() { + return exception; + } +} diff --git a/driver/src/main/java/org/neo4j/driver/exceptions/TokenExpiredRetryableException.java b/driver/src/main/java/org/neo4j/driver/exceptions/TokenExpiredRetryableException.java deleted file mode 100644 index a2a983656c..0000000000 --- a/driver/src/main/java/org/neo4j/driver/exceptions/TokenExpiredRetryableException.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * 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.neo4j.driver.exceptions; - -import java.io.Serial; -import org.neo4j.driver.AuthTokenManager; -import org.neo4j.driver.util.Preview; - -/** - * The token provided by the {@link AuthTokenManager} has expired. - *

- * This is a retryable variant of {@link TokenExpiredException} used when the driver has an explicit - * {@link AuthTokenManager} that might supply a new token following this failure. - *

- * Error code: Neo.ClientError.Security.TokenExpired - * @since 5.8 - * @see TokenExpiredException - * @see AuthTokenManager - * @see org.neo4j.driver.GraphDatabase#driver(String, AuthTokenManager) - */ -@Preview(name = "AuthToken rotation and session auth support") -public class TokenExpiredRetryableException extends TokenExpiredException implements RetryableException { - @Serial - private static final long serialVersionUID = -6672756500436910942L; - - /** - * Constructs a new instance. - * @param code the code - * @param message the message - */ - public TokenExpiredRetryableException(String code, String message) { - super(code, message); - } -} diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java index d62418645a..51c57974a4 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcher.java @@ -36,13 +36,13 @@ import org.neo4j.driver.Value; import org.neo4j.driver.exceptions.AuthorizationExpiredException; import org.neo4j.driver.exceptions.ClientException; +import org.neo4j.driver.exceptions.SecurityException; +import org.neo4j.driver.exceptions.SecurityRetryableException; import org.neo4j.driver.exceptions.TokenExpiredException; -import org.neo4j.driver.exceptions.TokenExpiredRetryableException; import org.neo4j.driver.internal.handlers.ResetResponseHandler; import org.neo4j.driver.internal.logging.ChannelActivityLogger; import org.neo4j.driver.internal.logging.ChannelErrorLogger; import org.neo4j.driver.internal.messaging.ResponseMessageHandler; -import org.neo4j.driver.internal.security.StaticAuthTokenManager; import org.neo4j.driver.internal.spi.ResponseHandler; import org.neo4j.driver.internal.util.ErrorUtil; @@ -120,20 +120,26 @@ public void handleFailureMessage(String code, String message) { } var currentError = this.currentError; - if (currentError instanceof AuthorizationExpiredException) { - authorizationStateListener(channel).onExpired(); - } else if (currentError instanceof TokenExpiredException tokenExpiredException) { - var authContext = authContext(channel); - var authTokenProvider = authContext.getAuthTokenManager(); - if (!(authTokenProvider instanceof StaticAuthTokenManager)) { - currentError = new TokenExpiredRetryableException( - tokenExpiredException.code(), tokenExpiredException.getMessage()); + var sendReset = true; + + if (currentError instanceof SecurityException securityException) { + if (securityException instanceof AuthorizationExpiredException) { + authorizationStateListener(channel).onExpired(); + sendReset = false; + } else if (securityException instanceof TokenExpiredException) { + sendReset = false; } + var authContext = authContext(channel); + var authTokenManager = authContext.getAuthTokenManager(); var authToken = authContext.getAuthToken(); if (authToken != null && authContext.isManaged()) { - authTokenProvider.onExpired(authToken); + if (authTokenManager.handleSecurityException(authToken, securityException)) { + currentError = new SecurityRetryableException(securityException); + } } - } else { + } + + if (sendReset) { // write a RESET to "acknowledge" the failure enqueue(new ResetResponseHandler(this)); channel.writeAndFlush(RESET, channel.voidPromise()); diff --git a/driver/src/main/java/org/neo4j/driver/internal/retry/ExponentialBackoffRetryLogic.java b/driver/src/main/java/org/neo4j/driver/internal/retry/ExponentialBackoffRetryLogic.java index eabca2b381..ab43f3cfb9 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/retry/ExponentialBackoffRetryLogic.java +++ b/driver/src/main/java/org/neo4j/driver/internal/retry/ExponentialBackoffRetryLogic.java @@ -35,6 +35,7 @@ import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.Neo4jException; import org.neo4j.driver.exceptions.RetryableException; +import org.neo4j.driver.exceptions.SecurityRetryableException; import org.neo4j.driver.internal.util.Futures; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -163,7 +164,9 @@ protected boolean canRetryOn(Throwable error) { */ private static Throwable extractPossibleTerminationCause(Throwable error) { // Having a dedicated "TerminatedException" inheriting from ClientException might be a good idea. - if (error instanceof ClientException && error.getCause() != null) { + if (!(error instanceof SecurityRetryableException) + && error instanceof ClientException + && error.getCause() != null) { return error.getCause(); } return error; diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/ExpirationBasedAuthTokenManager.java b/driver/src/main/java/org/neo4j/driver/internal/security/ExpirationBasedAuthTokenManager.java index cb1d95944e..d706fb8f75 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/security/ExpirationBasedAuthTokenManager.java +++ b/driver/src/main/java/org/neo4j/driver/internal/security/ExpirationBasedAuthTokenManager.java @@ -23,6 +23,7 @@ import static org.neo4j.driver.internal.util.LockUtil.executeWithLock; import java.time.Clock; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicBoolean; @@ -32,20 +33,26 @@ import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokenAndExpiration; import org.neo4j.driver.AuthTokenManager; +import org.neo4j.driver.exceptions.SecurityException; public class ExpirationBasedAuthTokenManager implements AuthTokenManager { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private final Supplier> freshTokenSupplier; + private final Set> retryableExceptionClasses; private final Clock clock; private CompletableFuture tokenFuture; private AuthTokenAndExpiration token; public ExpirationBasedAuthTokenManager( - Supplier> freshTokenSupplier, Clock clock) { + Supplier> freshTokenSupplier, + Set> retryableExceptionClasses, + Clock clock) { this.freshTokenSupplier = freshTokenSupplier; + this.retryableExceptionClasses = retryableExceptionClasses; this.clock = clock; } + @Override public CompletionStage getToken() { var validTokenFuture = executeWithLock(lock.readLock(), this::getValidTokenFuture); if (validTokenFuture == null) { @@ -65,12 +72,18 @@ public CompletionStage getToken() { return validTokenFuture; } - public void onExpired(AuthToken authToken) { - executeWithLock(lock.writeLock(), () -> { - if (token != null && token.authToken().equals(authToken)) { - unsetTokenState(); - } - }); + @Override + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + var retryable = retryableExceptionClasses.stream() + .anyMatch(retryableExceptionClass -> retryableExceptionClass.isAssignableFrom(exception.getClass())); + if (retryable) { + executeWithLock(lock.writeLock(), () -> { + if (token != null && token.authToken().equals(authToken)) { + unsetTokenState(); + } + }); + } + return retryable; } private void handleUpstreamResult(AuthTokenAndExpiration authTokenAndExpiration, Throwable throwable) { diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/StaticAuthTokenManager.java b/driver/src/main/java/org/neo4j/driver/internal/security/StaticAuthTokenManager.java index ecbbd1c2d2..9eef7d759d 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/security/StaticAuthTokenManager.java +++ b/driver/src/main/java/org/neo4j/driver/internal/security/StaticAuthTokenManager.java @@ -22,13 +22,11 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.concurrent.atomic.AtomicBoolean; import org.neo4j.driver.AuthToken; import org.neo4j.driver.AuthTokenManager; -import org.neo4j.driver.exceptions.TokenExpiredException; +import org.neo4j.driver.exceptions.SecurityException; public class StaticAuthTokenManager implements AuthTokenManager { - private final AtomicBoolean expired = new AtomicBoolean(); private final AuthToken authToken; public StaticAuthTokenManager(AuthToken authToken) { @@ -38,15 +36,11 @@ public StaticAuthTokenManager(AuthToken authToken) { @Override public CompletionStage getToken() { - return expired.get() - ? CompletableFuture.failedFuture(new TokenExpiredException(null, "authToken is expired")) - : CompletableFuture.completedFuture(authToken); + return CompletableFuture.completedFuture(authToken); } @Override - public void onExpired(AuthToken authToken) { - if (authToken.equals(this.authToken)) { - expired.set(true); - } + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; } } diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManager.java b/driver/src/main/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManager.java index 9ac60a4ce9..a7b5c89435 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManager.java +++ b/driver/src/main/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManager.java @@ -29,6 +29,7 @@ import org.neo4j.driver.Logger; import org.neo4j.driver.Logging; import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException; +import org.neo4j.driver.exceptions.SecurityException; public class ValidatingAuthTokenManager implements AuthTokenManager { private final Logger log; @@ -68,10 +69,12 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) { + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { requireNonNull(authToken, "authToken must not be null"); + requireNonNull(exception, "exception must not be null"); + var retryable = false; try { - delegate.onExpired(authToken); + retryable = delegate.handleSecurityException(authToken, exception); } catch (Throwable throwable) { log.warn(String.format( "%s has been thrown by %s.onExpired method", @@ -82,5 +85,6 @@ public void onExpired(AuthToken authToken) { throwable.getClass().getName(), delegate.getClass().getName()), throwable); } + return retryable; } } diff --git a/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthClusterIT.java b/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthClusterIT.java index a9db6a0dcf..fdc6b00b79 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthClusterIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthClusterIT.java @@ -35,6 +35,7 @@ import org.neo4j.driver.async.AsyncSession; import org.neo4j.driver.async.ResultCursor; import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException; +import org.neo4j.driver.exceptions.SecurityException; import org.neo4j.driver.reactive.ReactiveSession; import org.neo4j.driver.testutil.cc.LocalOrRemoteClusterExtension; import reactor.core.publisher.Mono; @@ -54,7 +55,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager); var session = driver.session()) { @@ -72,7 +75,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager); var session = driver.session()) { @@ -92,7 +97,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager); var session = driver.session()) { @@ -113,7 +120,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager); var session = driver.session()) { @@ -134,7 +143,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager); var session = driver.session()) { @@ -156,7 +167,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager); var session = driver.session()) { @@ -177,7 +190,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(AsyncSession.class); @@ -198,7 +213,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(AsyncSession.class); @@ -224,7 +241,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(AsyncSession.class); @@ -254,7 +273,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(AsyncSession.class); @@ -278,7 +299,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(AsyncSession.class); @@ -307,7 +330,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(AsyncSession.class); @@ -338,7 +363,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -358,7 +385,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -383,7 +412,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -412,7 +443,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -435,7 +468,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -463,7 +498,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -493,7 +530,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -513,7 +552,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -538,7 +579,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -567,7 +610,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -590,7 +635,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -618,7 +665,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(clusterRule.getClusterUri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); diff --git a/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthDirectIT.java b/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthDirectIT.java index 13ba46d749..17615efb6f 100644 --- a/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthDirectIT.java +++ b/driver/src/test/java/org/neo4j/driver/integration/GraphDatabaseAuthDirectIT.java @@ -35,6 +35,7 @@ import org.neo4j.driver.async.AsyncSession; import org.neo4j.driver.async.ResultCursor; import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException; +import org.neo4j.driver.exceptions.SecurityException; import org.neo4j.driver.reactive.ReactiveSession; import org.neo4j.driver.testutil.DatabaseExtension; import reactor.core.publisher.Mono; @@ -53,7 +54,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager); var session = driver.session()) { @@ -71,7 +74,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager); var session = driver.session()) { @@ -91,7 +96,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager); var session = driver.session()) { @@ -112,7 +119,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager); var session = driver.session()) { @@ -133,7 +142,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager); var session = driver.session()) { @@ -155,7 +166,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager); var session = driver.session()) { @@ -176,7 +189,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(AsyncSession.class); @@ -197,7 +212,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(AsyncSession.class); @@ -223,7 +240,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(AsyncSession.class); @@ -253,7 +272,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(AsyncSession.class); @@ -277,7 +298,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(AsyncSession.class); @@ -306,7 +329,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(AsyncSession.class); @@ -337,7 +362,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -357,7 +384,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -382,7 +411,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -411,7 +442,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -434,7 +467,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -462,7 +497,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(ReactiveSession.class); @@ -492,7 +529,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -512,7 +551,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -537,7 +578,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -566,7 +609,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -589,7 +634,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); @@ -617,7 +664,9 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) {} + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { + return false; + } }; try (var driver = GraphDatabase.driver(neo4j.uri(), manager)) { var session = driver.session(org.neo4j.driver.reactivestreams.ReactiveSession.class); diff --git a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java index 29b70cd6d8..435c45c14a 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/async/inbound/InboundMessageDispatcherTest.java @@ -64,8 +64,8 @@ import org.neo4j.driver.Values; import org.neo4j.driver.exceptions.ClientException; import org.neo4j.driver.exceptions.Neo4jException; +import org.neo4j.driver.exceptions.SecurityException; import org.neo4j.driver.exceptions.TokenExpiredException; -import org.neo4j.driver.exceptions.TokenExpiredRetryableException; import org.neo4j.driver.internal.async.pool.AuthContext; import org.neo4j.driver.internal.logging.ChannelActivityLogger; import org.neo4j.driver.internal.logging.ChannelErrorLogger; @@ -111,7 +111,15 @@ void shouldDequeHandlerOnSuccess() { @Test void shouldDequeHandlerOnFailure() { - var dispatcher = newDispatcher(); + var channel = new EmbeddedChannel(); + var authToken = AuthTokens.basic("username", "password"); + var authTokenManager = spy(new StaticAuthTokenManager(authToken)); + var authContext = mock(AuthContext.class); + given(authContext.isManaged()).willReturn(true); + given(authContext.getAuthTokenManager()).willReturn(authTokenManager); + given(authContext.getAuthToken()).willReturn(authToken); + setAuthContext(channel, authContext); + var dispatcher = newDispatcher(channel); var handler = mock(ResponseHandler.class); dispatcher.enqueue(handler); @@ -128,7 +136,14 @@ void shouldDequeHandlerOnFailure() { @Test void shouldSendResetOnFailure() { - var channel = newChannelMock(); + var channel = spy(new EmbeddedChannel()); + var authToken = AuthTokens.basic("username", "password"); + var authTokenManager = spy(new StaticAuthTokenManager(authToken)); + var authContext = mock(AuthContext.class); + given(authContext.isManaged()).willReturn(true); + given(authContext.getAuthTokenManager()).willReturn(authTokenManager); + given(authContext.getAuthToken()).willReturn(authToken); + setAuthContext(channel, authContext); var dispatcher = newDispatcher(channel); dispatcher.enqueue(mock(ResponseHandler.class)); @@ -141,7 +156,15 @@ void shouldSendResetOnFailure() { @Test void shouldClearFailureOnSuccessOfResetAfterFailure() { - var dispatcher = newDispatcher(); + var channel = new EmbeddedChannel(); + var authToken = AuthTokens.basic("username", "password"); + var authTokenManager = spy(new StaticAuthTokenManager(authToken)); + var authContext = mock(AuthContext.class); + given(authContext.isManaged()).willReturn(true); + given(authContext.getAuthTokenManager()).willReturn(authTokenManager); + given(authContext.getAuthToken()).willReturn(authToken); + setAuthContext(channel, authContext); + var dispatcher = newDispatcher(channel); dispatcher.enqueue(mock(ResponseHandler.class)); assertEquals(1, dispatcher.queuedHandlersCount()); @@ -239,7 +262,15 @@ void shouldDequeHandlerOnIgnored() { @Test void shouldFailHandlerOnIgnoredMessageWithExistingError() { - var dispatcher = newDispatcher(); + var channel = new EmbeddedChannel(); + var authToken = AuthTokens.basic("username", "password"); + var authTokenManager = spy(new StaticAuthTokenManager(authToken)); + var authContext = mock(AuthContext.class); + given(authContext.isManaged()).willReturn(true); + given(authContext.getAuthTokenManager()).willReturn(authTokenManager); + given(authContext.getAuthToken()).willReturn(authToken); + setAuthContext(channel, authContext); + var dispatcher = newDispatcher(channel); var handler1 = mock(ResponseHandler.class); var handler2 = mock(ResponseHandler.class); @@ -267,7 +298,15 @@ void shouldFailHandlerOnIgnoredMessageWhenNoErrorAndNotHandlingReset() { @Test void shouldDequeAndFailHandlerOnIgnoredWhenErrorHappened() { - var dispatcher = newDispatcher(); + var channel = new EmbeddedChannel(); + var authToken = AuthTokens.basic("username", "password"); + var authTokenManager = spy(new StaticAuthTokenManager(authToken)); + var authContext = mock(AuthContext.class); + given(authContext.isManaged()).willReturn(true); + given(authContext.getAuthTokenManager()).willReturn(authTokenManager); + given(authContext.getAuthToken()).willReturn(authToken); + setAuthContext(channel, authContext); + var dispatcher = newDispatcher(channel); var handler1 = mock(ResponseHandler.class); var handler2 = mock(ResponseHandler.class); @@ -366,7 +405,14 @@ void shouldReEnableAutoReadWhenAutoReadManagingHandlerIsRemoved() { @ValueSource(classes = {SuccessMessage.class, FailureMessage.class, RecordMessage.class, IgnoredMessage.class}) void shouldCreateChannelActivityLoggerAndLogDebugMessageOnMessageHandling(Class message) { // GIVEN - var channel = newChannelMock(); + var channel = new EmbeddedChannel(); + var authToken = AuthTokens.basic("username", "password"); + var authTokenManager = spy(new StaticAuthTokenManager(authToken)); + var authContext = mock(AuthContext.class); + given(authContext.isManaged()).willReturn(true); + given(authContext.getAuthTokenManager()).willReturn(authTokenManager); + given(authContext.getAuthToken()).willReturn(authToken); + setAuthContext(channel, authContext); var logging = mock(Logging.class); var logger = mock(Logger.class); when(logger.isDebugEnabled()).thenReturn(true); @@ -460,10 +506,11 @@ void shouldEmitTokenExpiredRetryableExceptionAndNotifyAuthTokenManager() { // then assertEquals(0, dispatcher.queuedHandlersCount()); - verifyFailure(handler, code, message, TokenExpiredRetryableException.class); + verifyFailure(handler, code, message, TokenExpiredException.class); assertEquals(code, ((Neo4jException) dispatcher.currentError()).code()); assertEquals(message, dispatcher.currentError().getMessage()); - then(authTokenManager).should().onExpired(authToken); + then(authTokenManager).should().handleSecurityException(authToken, (SecurityException) + dispatcher.currentError()); } @Test @@ -491,7 +538,8 @@ void shouldEmitTokenExpiredExceptionAndNotifyAuthTokenManager() { verifyFailure(handler, code, message, TokenExpiredException.class); assertEquals(code, ((Neo4jException) dispatcher.currentError()).code()); assertEquals(message, dispatcher.currentError().getMessage()); - then(authTokenManager).should().onExpired(authToken); + then(authTokenManager).should().handleSecurityException(authToken, (SecurityException) + dispatcher.currentError()); } private static void verifyFailure(ResponseHandler handler) { diff --git a/driver/src/test/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManagerTest.java b/driver/src/test/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManagerTest.java index e03abfb605..51b67616bd 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManagerTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/security/ValidatingAuthTokenManagerTest.java @@ -36,6 +36,7 @@ import org.neo4j.driver.Logger; import org.neo4j.driver.Logging; import org.neo4j.driver.exceptions.AuthTokenManagerExecutionException; +import org.neo4j.driver.exceptions.SecurityException; class ValidatingAuthTokenManagerTest { @Test @@ -103,47 +104,61 @@ void shouldPassOriginalToken() { } @Test - void shouldRejectNullAuthTokenOnExpiration() { + void shouldRejectNullAuthTokenOnHandleSecurityException() { // given var delegateManager = mock(AuthTokenManager.class); var manager = new ValidatingAuthTokenManager(delegateManager, Logging.none()); // when & then - assertThrows(NullPointerException.class, () -> manager.onExpired(null)); + assertThrows( + NullPointerException.class, () -> manager.handleSecurityException(null, mock(SecurityException.class))); then(delegateManager).shouldHaveNoInteractions(); } @Test - void shouldPassOriginalTokenOnExpiration() { + void shouldRejectNullExceptionOnHandleSecurityException() { + // given + var delegateManager = mock(AuthTokenManager.class); + var manager = new ValidatingAuthTokenManager(delegateManager, Logging.none()); + + // when & then + assertThrows(NullPointerException.class, () -> manager.handleSecurityException(AuthTokens.none(), null)); + then(delegateManager).shouldHaveNoInteractions(); + } + + @Test + void shouldPassOriginalTokenAndExceptionOnHandleSecurityException() { // given var delegateManager = mock(AuthTokenManager.class); var manager = new ValidatingAuthTokenManager(delegateManager, Logging.none()); var token = AuthTokens.none(); + var exception = mock(SecurityException.class); // when - manager.onExpired(token); + manager.handleSecurityException(token, exception); // then - then(delegateManager).should().onExpired(token); + then(delegateManager).should().handleSecurityException(token, exception); } @Test - void shouldLogWhenDelegateOnExpiredFails() { + void shouldLogWhenDelegateHandleSecurityExceptionFails() { // given var delegateManager = mock(AuthTokenManager.class); var token = AuthTokens.none(); + var securityException = mock(SecurityException.class); var exception = mock(RuntimeException.class); - willThrow(exception).given(delegateManager).onExpired(token); + willThrow(exception).given(delegateManager).handleSecurityException(token, securityException); var logging = mock(Logging.class); var log = mock(Logger.class); given(logging.getLog(ValidatingAuthTokenManager.class)).willReturn(log); var manager = new ValidatingAuthTokenManager(delegateManager, logging); // when - manager.onExpired(token); + manager.handleSecurityException(token, securityException); // then - then(delegateManager).should().onExpired(token); + then(delegateManager).should().handleSecurityException(token, securityException); then(log).should().warn(anyString()); then(log).should().debug(anyString(), eq(exception)); } diff --git a/intellij-code-inspection-driver-profile.xml b/intellij-code-inspection-driver-profile.xml index 2379ccce87..4245680d04 100644 --- a/intellij-code-inspection-driver-profile.xml +++ b/intellij-code-inspection-driver-profile.xml @@ -10,6 +10,9 @@ + + + diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/TestkitRequestProcessorHandler.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/TestkitRequestProcessorHandler.java index 535528be2f..e327817ad2 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/TestkitRequestProcessorHandler.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/channel/handler/TestkitRequestProcessorHandler.java @@ -38,6 +38,7 @@ import org.neo4j.driver.Logging; import org.neo4j.driver.exceptions.Neo4jException; import org.neo4j.driver.exceptions.NoSuchRecordException; +import org.neo4j.driver.exceptions.RetryableException; import org.neo4j.driver.exceptions.UntrustedServerException; import org.neo4j.driver.internal.spi.ConnectionPool; @@ -116,6 +117,7 @@ private TestkitResponse createErrorResponse(Throwable throwable) { .errorType(e.getClass().getName()) .code(e.code()) .msg(e.getMessage()) + .retryable(e instanceof RetryableException) .build()) .build(); } else if (isConnectionPoolClosedException(throwable) diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/AuthTokenManagerHandleSecurityExceptionCompleted.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/AuthTokenManagerHandleSecurityExceptionCompleted.java new file mode 100644 index 0000000000..05f3ab0c1c --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/AuthTokenManagerHandleSecurityExceptionCompleted.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 neo4j.org.testkit.backend.messages.requests; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class AuthTokenManagerHandleSecurityExceptionCompleted implements TestkitCallbackResult { + private AuthTokenManagerHandleSecurityExceptionCompletedBody data; + + @Override + public String getCallbackId() { + return data.getRequestId(); + } + + @Setter + @Getter + public static class AuthTokenManagerHandleSecurityExceptionCompletedBody { + private String requestId; + private boolean handled; + } +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/AuthTokenManagerOnAuthExpiredCompleted.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/BasicAuthTokenProviderCompleted.java similarity index 79% rename from testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/AuthTokenManagerOnAuthExpiredCompleted.java rename to testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/BasicAuthTokenProviderCompleted.java index 304eb31f56..10967b9c87 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/AuthTokenManagerOnAuthExpiredCompleted.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/BasicAuthTokenProviderCompleted.java @@ -23,8 +23,8 @@ @Setter @Getter -public class AuthTokenManagerOnAuthExpiredCompleted implements TestkitCallbackResult { - private AuthTokenManagerOnAuthExpiredCompletedBody data; +public class BasicAuthTokenProviderCompleted implements TestkitCallbackResult { + private BasicAuthTokenProviderCompletedBody data; @Override public String getCallbackId() { @@ -33,7 +33,8 @@ public String getCallbackId() { @Setter @Getter - public static class AuthTokenManagerOnAuthExpiredCompletedBody { + public static class BasicAuthTokenProviderCompletedBody { private String requestId; + private AuthorizationToken auth; } } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExpirationBasedAuthTokenProviderCompleted.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/BearerAuthTokenProviderCompleted.java similarity index 81% rename from testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExpirationBasedAuthTokenProviderCompleted.java rename to testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/BearerAuthTokenProviderCompleted.java index 5c1b6533cf..f095acd078 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExpirationBasedAuthTokenProviderCompleted.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/BearerAuthTokenProviderCompleted.java @@ -23,8 +23,8 @@ @Setter @Getter -public class ExpirationBasedAuthTokenProviderCompleted implements TestkitCallbackResult { - private ExpirationBasedAuthTokenProviderCompletedBody data; +public class BearerAuthTokenProviderCompleted implements TestkitCallbackResult { + private BearerAuthTokenProviderCompletedBody data; @Override public String getCallbackId() { @@ -33,7 +33,7 @@ public String getCallbackId() { @Setter @Getter - public static class ExpirationBasedAuthTokenProviderCompletedBody { + public static class BearerAuthTokenProviderCompletedBody { private String requestId; private AuthTokenAndExpiration auth; } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java index 1b320b7f00..16ad0764df 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/GetFeatures.java @@ -68,7 +68,8 @@ public class GetFeatures implements TestkitRequest { "Backend:MockTime", "Feature:API:Session:AuthConfig", "Feature:Auth:Managed", - "Feature:API:Driver.SupportsSessionAuth")); + "Feature:API:Driver.SupportsSessionAuth", + "Feature:API:RetryableExceptions")); private static final Set SYNC_FEATURES = new HashSet<>(Arrays.asList( "Feature:Bolt:3.0", diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewAuthTokenManager.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewAuthTokenManager.java index f720faaea9..a939d30a51 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewAuthTokenManager.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewAuthTokenManager.java @@ -26,11 +26,12 @@ import neo4j.org.testkit.backend.TestkitState; import neo4j.org.testkit.backend.messages.responses.AuthTokenManager; import neo4j.org.testkit.backend.messages.responses.AuthTokenManagerGetAuthRequest; -import neo4j.org.testkit.backend.messages.responses.AuthTokenManagerOnAuthExpiredRequest; -import neo4j.org.testkit.backend.messages.responses.AuthTokenManagerOnAuthExpiredRequest.AuthTokenManagerOnAuthExpiredRequestBody; +import neo4j.org.testkit.backend.messages.responses.AuthTokenManagerHandleSecurityExceptionRequest; +import neo4j.org.testkit.backend.messages.responses.AuthTokenManagerHandleSecurityExceptionRequest.AuthTokenManagerHandleSecurityExceptionRequestBody; import neo4j.org.testkit.backend.messages.responses.TestkitCallback; import neo4j.org.testkit.backend.messages.responses.TestkitResponse; import org.neo4j.driver.AuthToken; +import org.neo4j.driver.exceptions.SecurityException; @Setter @Getter @@ -74,23 +75,28 @@ public CompletionStage getToken() { } @Override - public void onExpired(AuthToken authToken) { + public boolean handleSecurityException(AuthToken authToken, SecurityException exception) { var callbackId = testkitState.newId(); - var callback = AuthTokenManagerOnAuthExpiredRequest.builder() - .data(AuthTokenManagerOnAuthExpiredRequestBody.builder() + var callback = AuthTokenManagerHandleSecurityExceptionRequest.builder() + .data(AuthTokenManagerHandleSecurityExceptionRequestBody.builder() .id(callbackId) .authTokenManagerId(authProviderId) .auth(AuthTokenUtil.parseAuthToken(authToken)) + .errorCode(exception.code()) .build()) .build(); var callbackStage = dispatchTestkitCallback(testkitState, callback); try { - callbackStage.toCompletableFuture().get(); + var response = callbackStage.toCompletableFuture().get(); + if (response instanceof AuthTokenManagerHandleSecurityExceptionCompleted authComplete) { + return authComplete.getData().isHandled(); + } } catch (Exception e) { throw new RuntimeException("Unexpected failure during Testkit callback", e); } + return false; } private CompletionStage dispatchTestkitCallback( diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewBasicAuthTokenManager.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewBasicAuthTokenManager.java new file mode 100644 index 0000000000..87680e8b14 --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewBasicAuthTokenManager.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 neo4j.org.testkit.backend.messages.requests; + +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; +import lombok.Getter; +import lombok.Setter; +import neo4j.org.testkit.backend.AuthTokenUtil; +import neo4j.org.testkit.backend.TestkitClock; +import neo4j.org.testkit.backend.TestkitState; +import neo4j.org.testkit.backend.messages.responses.BasicAuthTokenManager; +import neo4j.org.testkit.backend.messages.responses.BasicAuthTokenManager.BasicAuthTokenManagerBody; +import neo4j.org.testkit.backend.messages.responses.BasicAuthTokenProviderRequest; +import neo4j.org.testkit.backend.messages.responses.BasicAuthTokenProviderRequest.BasicAuthTokenProviderRequestBody; +import neo4j.org.testkit.backend.messages.responses.TestkitCallback; +import neo4j.org.testkit.backend.messages.responses.TestkitResponse; +import org.neo4j.driver.AuthTokenAndExpiration; +import org.neo4j.driver.exceptions.AuthenticationException; +import org.neo4j.driver.internal.security.ExpirationBasedAuthTokenManager; + +@Setter +@Getter +public class NewBasicAuthTokenManager extends AbstractBasicTestkitRequest { + private NewBasicAuthTokenManagerBody data; + + @Override + protected TestkitResponse processAndCreateResponse(TestkitState testkitState) { + var id = testkitState.newId(); + testkitState.addAuthProvider( + id, + new ExpirationBasedAuthTokenManager( + new TestkitAuthTokenProvider(id, testkitState), + Set.of(AuthenticationException.class), + TestkitClock.INSTANCE)); + return BasicAuthTokenManager.builder() + .data(BasicAuthTokenManagerBody.builder().id(id).build()) + .build(); + } + + private record TestkitAuthTokenProvider(String authProviderId, TestkitState testkitState) + implements Supplier> { + @Override + public CompletionStage get() { + var callbackId = testkitState.newId(); + + var callback = BasicAuthTokenProviderRequest.builder() + .data(BasicAuthTokenProviderRequestBody.builder() + .id(callbackId) + .basicAuthTokenManagerId(authProviderId) + .build()) + .build(); + + var callbackStage = dispatchTestkitCallback(testkitState, callback); + BasicAuthTokenProviderCompleted resolutionCompleted; + try { + resolutionCompleted = (BasicAuthTokenProviderCompleted) + callbackStage.toCompletableFuture().get(); + } catch (Exception e) { + throw new RuntimeException("Unexpected failure during Testkit callback", e); + } + + var authToken = + AuthTokenUtil.parseAuthToken(resolutionCompleted.getData().getAuth()); + return CompletableFuture.completedFuture(authToken.expiringAt(Long.MAX_VALUE)); + } + + private CompletionStage dispatchTestkitCallback( + TestkitState testkitState, TestkitCallback response) { + var future = new CompletableFuture(); + testkitState.getCallbackIdToFuture().put(response.getCallbackId(), future); + testkitState.getResponseWriter().accept(response); + return future; + } + } + + @Setter + @Getter + public static class NewBasicAuthTokenManagerBody {} +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/NewExpirationBasedAuthTokenManager.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewBearerAuthTokenManager.java similarity index 70% rename from testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/NewExpirationBasedAuthTokenManager.java rename to testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewBearerAuthTokenManager.java index 96e7b20d9b..c93dbcfe84 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/NewExpirationBasedAuthTokenManager.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/NewBearerAuthTokenManager.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package neo4j.org.testkit.backend.messages.responses; +package neo4j.org.testkit.backend.messages.requests; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Supplier; @@ -26,17 +27,19 @@ import neo4j.org.testkit.backend.AuthTokenUtil; import neo4j.org.testkit.backend.TestkitClock; import neo4j.org.testkit.backend.TestkitState; -import neo4j.org.testkit.backend.messages.requests.AbstractBasicTestkitRequest; -import neo4j.org.testkit.backend.messages.requests.ExpirationBasedAuthTokenProviderCompleted; -import neo4j.org.testkit.backend.messages.requests.ExpirationBasedAuthTokenProviderRequest; -import neo4j.org.testkit.backend.messages.requests.TestkitCallbackResult; +import neo4j.org.testkit.backend.messages.responses.BearerAuthTokenManager; +import neo4j.org.testkit.backend.messages.responses.BearerAuthTokenProviderRequest; +import neo4j.org.testkit.backend.messages.responses.TestkitCallback; +import neo4j.org.testkit.backend.messages.responses.TestkitResponse; import org.neo4j.driver.AuthTokenAndExpiration; +import org.neo4j.driver.exceptions.AuthenticationException; +import org.neo4j.driver.exceptions.TokenExpiredException; import org.neo4j.driver.internal.security.ExpirationBasedAuthTokenManager; @Setter @Getter -public class NewExpirationBasedAuthTokenManager extends AbstractBasicTestkitRequest { - private NewTemporalAuthTokenManagerBody data; +public class NewBearerAuthTokenManager extends AbstractBasicTestkitRequest { + private NewBearerAuthTokenManagerBody data; @Override protected TestkitResponse processAndCreateResponse(TestkitState testkitState) { @@ -44,10 +47,11 @@ protected TestkitResponse processAndCreateResponse(TestkitState testkitState) { testkitState.addAuthProvider( id, new ExpirationBasedAuthTokenManager( - new TestkitAuthTokenProvider(id, testkitState), TestkitClock.INSTANCE)); - return neo4j.org.testkit.backend.messages.responses.ExpirationBasedAuthTokenManager.builder() - .data(neo4j.org.testkit.backend.messages.responses.ExpirationBasedAuthTokenManager - .ExpirationBasedTokenManagerBody.builder() + new TestkitAuthTokenProvider(id, testkitState), + Set.of(TokenExpiredException.class, AuthenticationException.class), + TestkitClock.INSTANCE)); + return BearerAuthTokenManager.builder() + .data(BearerAuthTokenManager.BearerAuthTokenManagerBody.builder() .id(id) .build()) .build(); @@ -59,17 +63,17 @@ private record TestkitAuthTokenProvider(String authProviderId, TestkitState test public CompletionStage get() { var callbackId = testkitState.newId(); - var callback = ExpirationBasedAuthTokenProviderRequest.builder() - .data(ExpirationBasedAuthTokenProviderRequest.ExpirationBasedAuthTokenProviderRequestBody.builder() + var callback = BearerAuthTokenProviderRequest.builder() + .data(BearerAuthTokenProviderRequest.BearerAuthTokenProviderRequestBody.builder() .id(callbackId) - .expirationBasedAuthTokenManagerId(authProviderId) + .bearerAuthTokenManagerId(authProviderId) .build()) .build(); var callbackStage = dispatchTestkitCallback(testkitState, callback); - ExpirationBasedAuthTokenProviderCompleted resolutionCompleted; + BearerAuthTokenProviderCompleted resolutionCompleted; try { - resolutionCompleted = (ExpirationBasedAuthTokenProviderCompleted) + resolutionCompleted = (BearerAuthTokenProviderCompleted) callbackStage.toCompletableFuture().get(); } catch (Exception e) { throw new RuntimeException("Unexpected failure during Testkit callback", e); @@ -94,5 +98,5 @@ private CompletionStage dispatchTestkitCallback( @Setter @Getter - public static class NewTemporalAuthTokenManagerBody {} + public static class NewBearerAuthTokenManagerBody {} } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java index 3e210f55da..9860102cc8 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/StartTest.java @@ -159,6 +159,8 @@ public class StartTest implements TestkitRequest { "^.*\\.TestTxRun\\.test_should_prevent_run_after_tx_termination_on_run$", skipMessage); REACTIVE_LEGACY_SKIP_PATTERN_TO_REASON.put( "^.*\\.TestTxRun\\.test_should_prevent_run_after_tx_termination_on_pull$", skipMessage); + REACTIVE_LEGACY_SKIP_PATTERN_TO_REASON.put( + "^.*\\.TestAuthTokenManager[^.]+\\.test_error_on_run_using_tx_run$", skipMessage); skipMessage = "Does not support multiple concurrent result streams on session level"; REACTIVE_LEGACY_SKIP_PATTERN_TO_REASON.put("^.*\\.TestSessionRun\\.test_iteration_nested$", skipMessage); REACTIVE_LEGACY_SKIP_PATTERN_TO_REASON.put("^.*\\.TestSessionRun\\.test_partial_iteration$", skipMessage); diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java index c8e3237297..61458567e6 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/TestkitRequest.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import java.util.concurrent.CompletionStage; import neo4j.org.testkit.backend.TestkitState; -import neo4j.org.testkit.backend.messages.responses.NewExpirationBasedAuthTokenManager; import neo4j.org.testkit.backend.messages.responses.TestkitResponse; import reactor.core.publisher.Mono; @@ -65,10 +64,12 @@ @JsonSubTypes.Type(BookmarkManagerClose.class), @JsonSubTypes.Type(ExecuteQuery.class), @JsonSubTypes.Type(NewAuthTokenManager.class), - @JsonSubTypes.Type(NewExpirationBasedAuthTokenManager.class), + @JsonSubTypes.Type(NewBearerAuthTokenManager.class), + @JsonSubTypes.Type(NewBasicAuthTokenManager.class), @JsonSubTypes.Type(AuthTokenManagerGetAuthCompleted.class), - @JsonSubTypes.Type(ExpirationBasedAuthTokenProviderCompleted.class), - @JsonSubTypes.Type(AuthTokenManagerOnAuthExpiredCompleted.class), + @JsonSubTypes.Type(BearerAuthTokenProviderCompleted.class), + @JsonSubTypes.Type(BasicAuthTokenProviderCompleted.class), + @JsonSubTypes.Type(AuthTokenManagerHandleSecurityExceptionCompleted.class), @JsonSubTypes.Type(AuthTokenManagerClose.class), @JsonSubTypes.Type(FakeTimeInstall.class), @JsonSubTypes.Type(FakeTimeTick.class), diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/AuthTokenManagerOnAuthExpiredRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/AuthTokenManagerHandleSecurityExceptionRequest.java similarity index 81% rename from testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/AuthTokenManagerOnAuthExpiredRequest.java rename to testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/AuthTokenManagerHandleSecurityExceptionRequest.java index 6db6546d00..41421b84ef 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/AuthTokenManagerOnAuthExpiredRequest.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/AuthTokenManagerHandleSecurityExceptionRequest.java @@ -24,8 +24,8 @@ @Getter @Builder -public class AuthTokenManagerOnAuthExpiredRequest implements TestkitCallback { - private AuthTokenManagerOnAuthExpiredRequestBody data; +public class AuthTokenManagerHandleSecurityExceptionRequest implements TestkitCallback { + private AuthTokenManagerHandleSecurityExceptionRequestBody data; @Override public String testkitName() { @@ -39,9 +39,10 @@ public String getCallbackId() { @Getter @Builder - public static class AuthTokenManagerOnAuthExpiredRequestBody { + public static class AuthTokenManagerHandleSecurityExceptionRequestBody { private String id; private String authTokenManagerId; private AuthorizationToken auth; + private String errorCode; } } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ExpirationBasedAuthTokenManager.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BasicAuthTokenManager.java similarity index 79% rename from testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ExpirationBasedAuthTokenManager.java rename to testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BasicAuthTokenManager.java index e756b2cb1c..896d87cac7 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/ExpirationBasedAuthTokenManager.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BasicAuthTokenManager.java @@ -23,17 +23,17 @@ @Getter @Builder -public class ExpirationBasedAuthTokenManager implements TestkitResponse { - private ExpirationBasedTokenManagerBody data; +public class BasicAuthTokenManager implements TestkitResponse { + private BasicAuthTokenManagerBody data; @Override public String testkitName() { - return "ExpirationBasedAuthTokenManager"; + return "BasicAuthTokenManager"; } @Getter @Builder - public static class ExpirationBasedTokenManagerBody { + public static class BasicAuthTokenManagerBody { private String id; } } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExpirationBasedAuthTokenProviderRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BasicAuthTokenProviderRequest.java similarity index 67% rename from testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExpirationBasedAuthTokenProviderRequest.java rename to testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BasicAuthTokenProviderRequest.java index 03f383608d..0ba35f39b6 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/requests/ExpirationBasedAuthTokenProviderRequest.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BasicAuthTokenProviderRequest.java @@ -16,16 +16,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package neo4j.org.testkit.backend.messages.requests; +package neo4j.org.testkit.backend.messages.responses; import lombok.Builder; import lombok.Getter; -import neo4j.org.testkit.backend.messages.responses.TestkitCallback; @Getter @Builder -public class ExpirationBasedAuthTokenProviderRequest implements TestkitCallback { - private ExpirationBasedAuthTokenProviderRequestBody data; +public class BasicAuthTokenProviderRequest implements TestkitCallback { + private BasicAuthTokenProviderRequestBody data; @Override public String getCallbackId() { @@ -34,13 +33,13 @@ public String getCallbackId() { @Override public String testkitName() { - return "ExpirationBasedAuthTokenProviderRequest"; + return "BasicAuthTokenProviderRequest"; } @Getter @Builder - public static class ExpirationBasedAuthTokenProviderRequestBody { + public static class BasicAuthTokenProviderRequestBody { private String id; - private String expirationBasedAuthTokenManagerId; + private String basicAuthTokenManagerId; } } diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BearerAuthTokenManager.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BearerAuthTokenManager.java new file mode 100644 index 0000000000..09e2453a2a --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BearerAuthTokenManager.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 neo4j.org.testkit.backend.messages.responses; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BearerAuthTokenManager implements TestkitResponse { + private BearerAuthTokenManagerBody data; + + @Override + public String testkitName() { + return "BearerAuthTokenManager"; + } + + @Getter + @Builder + public static class BearerAuthTokenManagerBody { + private String id; + } +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BearerAuthTokenProviderRequest.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BearerAuthTokenProviderRequest.java new file mode 100644 index 0000000000..35fcda1e65 --- /dev/null +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/BearerAuthTokenProviderRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * 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 neo4j.org.testkit.backend.messages.responses; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class BearerAuthTokenProviderRequest implements TestkitCallback { + private BearerAuthTokenProviderRequestBody data; + + @Override + public String getCallbackId() { + return data.getId(); + } + + @Override + public String testkitName() { + return "BearerAuthTokenProviderRequest"; + } + + @Getter + @Builder + public static class BearerAuthTokenProviderRequestBody { + private String id; + private String bearerAuthTokenManagerId; + } +} diff --git a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/DriverError.java b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/DriverError.java index 6883ef4c9d..4e7d52e066 100644 --- a/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/DriverError.java +++ b/testkit-backend/src/main/java/neo4j/org/testkit/backend/messages/responses/DriverError.java @@ -41,5 +41,7 @@ public static class DriverErrorBody { private String code; private String msg; + + private boolean retryable; } }