diff --git a/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java b/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java index 6a1c7c5c9c..4fe2a729d8 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java +++ b/driver/src/main/java/org/neo4j/driver/internal/cluster/Rediscovery.java @@ -24,6 +24,7 @@ import org.neo4j.driver.internal.util.Clock; import org.neo4j.driver.v1.Logger; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.exceptions.SecurityException; import static java.lang.String.format; @@ -75,7 +76,11 @@ public ClusterComposition lookupRoutingTable( ConnectionPool connections, Routin { response = provider.getClusterComposition( connection ); } - catch ( Throwable e ) + catch( SecurityException e ) + { + throw e; // terminate the discovery immediately + } + catch ( Exception e ) { // the connection breaks logger.error( format( "Failed to connect to routing server '%s'.", address ), e ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java b/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java index bbcc161369..2ca4e0f089 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java +++ b/driver/src/main/java/org/neo4j/driver/internal/net/SocketResponseHandler.java @@ -22,6 +22,7 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import org.neo4j.driver.v1.exceptions.AuthenticationException; import org.neo4j.driver.internal.messaging.MessageHandler; import org.neo4j.driver.internal.spi.Collector; import org.neo4j.driver.internal.summary.InternalNotification; @@ -65,7 +66,14 @@ public void handleFailureMessage( String code, String message ) switch ( classification ) { case "ClientError": - error = new ClientException( code, message ); + if( code.equalsIgnoreCase( "Neo.ClientError.Security.Unauthorized" ) ) + { + error = new AuthenticationException( code, message ); + } + else + { + error = new ClientException( code, message ); + } break; case "TransientError": error = new TransientException( code, message ); diff --git a/driver/src/main/java/org/neo4j/driver/internal/security/TLSSocketChannel.java b/driver/src/main/java/org/neo4j/driver/internal/security/TLSSocketChannel.java index ec0e8e9da0..5abea24f60 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/security/TLSSocketChannel.java +++ b/driver/src/main/java/org/neo4j/driver/internal/security/TLSSocketChannel.java @@ -32,6 +32,7 @@ import org.neo4j.driver.internal.util.BytePrinter; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.exceptions.SecurityException; import static java.lang.String.format; import static javax.net.ssl.SSLEngineResult.HandshakeStatus.FINISHED; @@ -81,7 +82,7 @@ public static TLSSocketChannel create( ByteChannel channel, Logger logger, SSLEn } catch ( SSLHandshakeException e ) { - throw new ClientException( "Failed to establish secured connection with the server: " + e.getMessage(), e ); + throw new SecurityException( "Failed to establish secured connection with the server: " + e.getMessage(), e ); } return tlsChannel; } diff --git a/driver/src/main/java/org/neo4j/driver/v1/exceptions/AuthenticationException.java b/driver/src/main/java/org/neo4j/driver/v1/exceptions/AuthenticationException.java new file mode 100644 index 0000000000..be5f32c22a --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/exceptions/AuthenticationException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1.exceptions; + +/** + * Failed to authenticate the driver to the server due to bad credentials provided. + * When this error happens, the error could be recovered by closing the current driver and restart a new driver with + * the correct credentials. + * + * @since 1.1 + */ +public class AuthenticationException extends SecurityException +{ + public AuthenticationException( String code, String message ) + { + super( code, message ); + } +} diff --git a/driver/src/main/java/org/neo4j/driver/v1/exceptions/SecurityException.java b/driver/src/main/java/org/neo4j/driver/v1/exceptions/SecurityException.java new file mode 100644 index 0000000000..6bce1105a2 --- /dev/null +++ b/driver/src/main/java/org/neo4j/driver/v1/exceptions/SecurityException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2002-2017 "Neo Technology," + * Network Engine for Objects in Lund AB [http://neotechnology.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.v1.exceptions; + +/** + * Failed to communicate with the server due to security errors. + * When this type of error happens, the security cause of the error should be fixed to ensure the safety of your data. + * Restart of server/driver/cluster might be required to recover from this error. + * @since 1.1 + */ +public class SecurityException extends Neo4jException +{ + public SecurityException( String code, String message ) + { + super( code, message ); + } + + public SecurityException( String message, Throwable t ) + { + super( message, t ); + } +} diff --git a/driver/src/test/java/org/neo4j/driver/internal/security/TLSSocketChannelTest.java b/driver/src/test/java/org/neo4j/driver/internal/security/TLSSocketChannelTest.java index 9468877986..9da91c13f0 100644 --- a/driver/src/test/java/org/neo4j/driver/internal/security/TLSSocketChannelTest.java +++ b/driver/src/test/java/org/neo4j/driver/internal/security/TLSSocketChannelTest.java @@ -26,8 +26,8 @@ import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; -import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.exceptions.SecurityException; import static junit.framework.TestCase.fail; import static org.hamcrest.MatcherAssert.assertThat; @@ -107,7 +107,7 @@ public void shouldCloseConnectionIfFailedToWrite() throws Throwable } @Test - public void shouldThrowClientErrorIfFailedToHandshake() throws Throwable + public void shouldThrowUnauthorizedIfFailedToHandshake() throws Throwable { // Given ByteChannel mockedChannel = mock( ByteChannel.class ); @@ -128,7 +128,7 @@ public void shouldThrowClientErrorIfFailedToHandshake() throws Throwable } catch( Exception e ) { - assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e, instanceOf( SecurityException.class ) ); assertThat( e.getMessage(), startsWith( "Failed to establish secured connection with the server: Failed handshake!" ) ); } verify( mockedChannel, never() ).close(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java index c9e47c1164..f9a69e1064 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/CredentialsIT.java @@ -30,7 +30,7 @@ import org.neo4j.driver.v1.GraphDatabase; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.Value; -import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.exceptions.SecurityException; import org.neo4j.driver.v1.util.Neo4jSettings; import org.neo4j.driver.v1.util.TestNeo4j; @@ -79,7 +79,7 @@ public void shouldGetHelpfulErrorOnInvalidCredentials() throws Throwable } catch( Throwable e ) { - assertThat( e, instanceOf( ClientException.class ) ); + assertThat( e, instanceOf( SecurityException.class ) ); assertThat( e.getMessage(), containsString( "The client is unauthorized due to authentication failure." ) ); } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java index 4d5c1b25f2..1c89199bf8 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TLSSocketChannelIT.java @@ -30,7 +30,6 @@ import java.net.InetSocketAddress; import java.nio.channels.SocketChannel; import java.security.cert.X509Certificate; -import javax.net.ssl.SSLHandshakeException; import org.neo4j.driver.internal.net.BoltServerAddress; import org.neo4j.driver.internal.security.SecurityPlan; @@ -43,7 +42,7 @@ import org.neo4j.driver.v1.Logging; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.exceptions.SecurityException; import org.neo4j.driver.v1.util.CertificateToolTest; import org.neo4j.driver.v1.util.Neo4jRunner; import org.neo4j.driver.v1.util.Neo4jSettings; @@ -53,6 +52,7 @@ import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -150,18 +150,25 @@ public void shouldNotPerformTLSHandshakeWithNonSystemCert() throws Throwable SecurityPlan securityPlan = SecurityPlan.forSystemCASignedCertificates(); // When + TLSSocketChannel sslChannel = null; try { - TLSSocketChannel sslChannel = - TLSSocketChannel.create(address, securityPlan, channel, logger); - sslChannel.close(); + sslChannel = TLSSocketChannel.create(address, securityPlan, channel, logger); + fail( "Should have thrown exception" ); } - catch ( ClientException e ) + catch ( SecurityException e ) { assertThat( e.getMessage(), containsString( "General SSLEngine problem" ) ); assertThat( getRootCause( e ).getMessage(), containsString( "unable to find valid certification path to requested target" ) ); } + finally + { + if( sslChannel != null ) + { + sslChannel.close(); + } + } } finally { @@ -188,12 +195,12 @@ public void shouldFailTLSHandshakeDueToWrongCertInKnownCertsFile() throws Throwa TLSSocketChannel sslChannel = null; try { - sslChannel = TLSSocketChannel.create( address, securityPlan, channel, mock( Logger.class ) ); - sslChannel.close(); + sslChannel = TLSSocketChannel.create( address, securityPlan, channel, DEV_NULL_LOGGER ); + fail( "Should have thrown exception" ); } - catch ( SSLHandshakeException e ) + catch ( SecurityException e ) { - assertEquals( "General SSLEngine problem", e.getMessage() ); + assertThat( e.getMessage(), containsString( "General SSLEngine problem" ) ); assertThat( getRootCause( e ).getMessage(), containsString( "If you trust the certificate the server uses now, simply remove the line that starts with" ) ); } @@ -209,14 +216,13 @@ public void shouldFailTLSHandshakeDueToWrongCertInKnownCertsFile() throws Throwa private void createFakeServerCertPairInKnownCerts( BoltServerAddress address, File knownCerts ) throws Throwable { - address = address.resolve(); // localhost -> 127.0.0.1 String serverId = address.toString(); X509Certificate cert = CertificateToolTest.generateSelfSignedCertificate(); String certStr = fingerprint(cert); BufferedWriter writer = new BufferedWriter( new FileWriter( knownCerts, true ) ); - writer.write( serverId + "," + certStr ); + writer.write( serverId + " " + certStr ); writer.newLine(); writer.close(); } @@ -241,9 +247,9 @@ public void shouldFailTLSHandshakeDueToServerCertNotSignedByKnownCA() throws Thr try { sslChannel = TLSSocketChannel.create( neo4j.address(), securityPlan, channel, mock( Logger.class ) ); - sslChannel.close(); + fail( "Should have thrown exception" ); } - catch ( ClientException e ) + catch ( SecurityException e ) { assertThat( e.getMessage(), containsString( "General SSLEngine problem" ) ); assertThat( getRootCause( e ).getMessage(), containsString( "No trusted certificate found" ) ); diff --git a/driver/src/test/java/org/neo4j/driver/v1/stress/CausalClusteringStressIT.java b/driver/src/test/java/org/neo4j/driver/v1/stress/CausalClusteringStressIT.java index 5171ec8850..84cb14e428 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/stress/CausalClusteringStressIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/stress/CausalClusteringStressIT.java @@ -51,7 +51,7 @@ import org.neo4j.driver.v1.StatementResult; import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.exceptions.ClientException; -import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; +import org.neo4j.driver.v1.exceptions.SecurityException; import org.neo4j.driver.v1.types.Node; import org.neo4j.driver.v1.util.DaemonThreadFactory; import org.neo4j.driver.v1.util.cc.LocalOrRemoteClusterRule; @@ -411,14 +411,12 @@ public void execute() } catch ( Exception e ) { - assertThat( e, instanceOf( ServiceUnavailableException.class ) ); + assertThat( e, instanceOf( SecurityException.class ) ); + assertThat( e.getMessage(), containsString( "authentication failure" ) ); ArgumentCaptor captor = ArgumentCaptor.forClass( Throwable.class ); - verify( logger ).error( startsWith( "Failed to connect to routing server" ), captor.capture() ); - - Throwable loggedThrowable = captor.getValue(); - assertThat( loggedThrowable, instanceOf( ClientException.class ) ); - assertThat( loggedThrowable.getMessage().toLowerCase(), containsString( "authentication failure" ) ); + verify( logger ).debug( startsWith( "~~ [CLOSED SECURE CHANNEL]" ), captor.capture() ); + verify( logger ).debug( startsWith( "~~ [DISCONNECT]" ), captor.capture() ); } } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java index df38c124a9..94c3a81a02 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java +++ b/driver/src/test/java/org/neo4j/driver/v1/tck/DriverSecurityComplianceSteps.java @@ -33,7 +33,7 @@ import org.neo4j.driver.v1.GraphDatabase; import org.neo4j.driver.v1.Session; import org.neo4j.driver.v1.StatementResult; -import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.exceptions.SecurityException; import org.neo4j.driver.v1.util.CertificateToolTest.CertificateSigningRequestGenerator; import org.neo4j.driver.v1.util.CertificateToolTest.SelfSignedCertificateGenerator; import org.neo4j.driver.v1.util.Neo4jRunner; @@ -134,7 +134,7 @@ public void creatingSessionsShouldFail() throws Throwable public void iShouldGetAHelpfulErrorExplainingThatCertificateChanged( String str ) throws Throwable { assertThat( exception, notNullValue() ); - assertThat( exception, instanceOf( ClientException.class ) ); + assertThat( exception, instanceOf( SecurityException.class ) ); Throwable rootCause = getRootCause( exception ); assertThat( rootCause.toString(), containsString( "Unable to connect to neo4j at `localhost:7687`, because the certificate the server uses has changed. " + @@ -244,7 +244,7 @@ public void aRunningNeo4jDatabaseUsingACertNotSignedByTheTrustedCertificates() t public void iShouldGetAHelpfulErrorExplainingThatCertificatedNotSigned() throws Throwable { assertThat( exception, notNullValue() ); - assertThat( exception, instanceOf( ClientException.class ) ); + assertThat( exception, instanceOf( SecurityException.class ) ); Throwable rootCause = getRootCause( exception ); assertThat( rootCause.toString(), containsString( "Signature does not match.") ); }