diff --git a/src/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryService.php b/src/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryService.php index d1cd8383..42c42bf2 100644 --- a/src/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryService.php +++ b/src/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryService.php @@ -14,6 +14,7 @@ use ProfessionalWiki\NeoWiki\GraphDatabasePlugins\Neo4j\Application\Exception\QueryException; use ProfessionalWiki\NeoWiki\GraphDatabasePlugins\Neo4j\Application\Exception\QueryTimeoutException; use ProfessionalWiki\NeoWiki\GraphDatabasePlugins\Neo4j\Application\Exception\WriteQueryRejectedException; +use ProfessionalWiki\NeoWiki\Domain\GraphDatabase\BackendFailureMessage; use ProfessionalWiki\NeoWiki\GraphDatabasePlugins\Neo4j\Persistence\Neo4jResultNormalizer; use Throwable; @@ -41,7 +42,11 @@ public function execute( Neo4jQueryRequest $request ): Neo4jQueryResult { // not a misleading "backend unavailable". throw $this->translateNeo4jException( $e ); } catch ( Throwable $e ) { - throw new BackendUnavailableException( $e->getMessage(), 0, $e ); + throw new BackendUnavailableException( + BackendFailureMessage::withoutCredentials( $e->getMessage() ), + 0, + $e + ); } if ( !$allowed ) { @@ -59,7 +64,11 @@ public function execute( Neo4jQueryRequest $request ): Neo4jQueryResult { } catch ( Neo4jException $e ) { throw $this->translateNeo4jException( $e ); } catch ( Throwable $e ) { - throw new BackendUnavailableException( $e->getMessage(), 0, $e ); + throw new BackendUnavailableException( + BackendFailureMessage::withoutCredentials( $e->getMessage() ), + 0, + $e + ); } $durationMs = (int)( microtime( true ) * 1000 ) - $startedAt; diff --git a/tests/phpunit/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryServiceTest.php b/tests/phpunit/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryServiceTest.php index 8638d830..012ece34 100644 --- a/tests/phpunit/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryServiceTest.php +++ b/tests/phpunit/GraphDatabasePlugins/Neo4j/Application/Neo4jQueryServiceTest.php @@ -31,6 +31,13 @@ */ class Neo4jQueryServiceTest extends TestCase { + /** + * How the driver reports an unreachable server: by quoting the Bolt URI it dialed, credentials + * included. The query surfaces hand this message to callers, anonymous ones included. + */ + private const string UNREACHABLE_SERVER_MESSAGE = + "Cannot connect to any server on alias: default with Uris: ('bolt://neowiki_read:S3cr3t@graph.example:7687')"; + public function testReturnsRowsAsListWithColumnsFromProtocolKeys(): void { $service = $this->newService( $this->stubEngineWithRows( [ @@ -207,6 +214,44 @@ public function queryIsAllowed( string $cypher ): bool { $service->execute( $this->newRequest( 'MATCH (n) RETURN n' ) ); } + public function testEngineBackendFailureDoesNotCarryCredentials(): void { + $service = $this->newService( + $this->stubEngineThrowing( new RuntimeException( self::UNREACHABLE_SERVER_MESSAGE ) ) + ); + + $message = $this->executeAndCatch( $service )->getMessage(); + + $this->assertStringNotContainsString( 'S3cr3t', $message ); + $this->assertStringContainsString( 'graph.example', $message ); + } + + public function testValidatorBackendFailureDoesNotCarryCredentials(): void { + $throwingValidator = new class( self::UNREACHABLE_SERVER_MESSAGE ) implements CypherQueryValidator { + public function __construct( private string $message ) { + } + + public function queryIsAllowed( string $cypher ): bool { + throw new RuntimeException( $this->message ); + } + }; + $service = $this->newService( $this->stubEngineWithRows( [] ), validator: $throwingValidator ); + + $message = $this->executeAndCatch( $service )->getMessage(); + + $this->assertStringNotContainsString( 'S3cr3t', $message ); + $this->assertStringContainsString( 'graph.example', $message ); + } + + private function executeAndCatch( Neo4jQueryService $service ): BackendUnavailableException { + try { + $service->execute( $this->newRequest( 'MATCH (n) RETURN n' ) ); + } catch ( BackendUnavailableException $e ) { + return $e; + } + + $this->fail( 'Expected a BackendUnavailableException.' ); + } + public function testValidatorNeo4jSyntaxErrorBecomesCypherSyntaxException(): void { $throwingValidator = new class implements CypherQueryValidator { public function queryIsAllowed( string $cypher ): bool {