Skip to content
This repository was archived by the owner on Jul 6, 2023. It is now read-only.

Commit 5494d30

Browse files
authored
Merge pull request #202 from pontusmelke/4.0-allow-password-change
Allow user to update expired password non-interactively
2 parents 25f095a + 9b3c2af commit 5494d30

File tree

15 files changed

+319
-307
lines changed

15 files changed

+319
-307
lines changed

cypher-shell/src/integration-test/java/org/neo4j/shell/MainIntegrationTest.java

Lines changed: 119 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.neo4j.driver.exceptions.ServiceUnavailableException;
1717
import org.neo4j.driver.exceptions.TransientException;
1818
import org.neo4j.shell.cli.CliArgs;
19+
import org.neo4j.shell.cli.Format;
1920
import org.neo4j.shell.commands.CommandHelper;
2021
import org.neo4j.shell.exception.CommandException;
2122
import org.neo4j.shell.exception.ExitException;
@@ -37,10 +38,16 @@
3738
import static org.mockito.Mockito.mock;
3839
import static org.mockito.Mockito.verify;
3940
import static org.mockito.Mockito.verifyNoMoreInteractions;
41+
import static org.neo4j.shell.DatabaseManager.DEFAULT_DEFAULT_DB_NAME;
42+
import static org.neo4j.shell.DatabaseManager.SYSTEM_DB_NAME;
43+
import static org.neo4j.shell.Main.EXIT_FAILURE;
44+
import static org.neo4j.shell.Main.EXIT_SUCCESS;
4045
import static org.neo4j.shell.util.Versions.majorVersion;
4146

4247
public class MainIntegrationTest
4348
{
49+
private static String USER = "neo4j";
50+
private static String PASSWORD = "neo";
4451

4552
private static class ShellAndConnection
4653
{
@@ -56,7 +63,7 @@ private static class ShellAndConnection
5663

5764
@Rule
5865
public final ExpectedException exception = ExpectedException.none();
59-
private String inputString = String.format( "neo4j%nneo%n" );
66+
private String inputString = String.format( "%s%n%s%n", USER, PASSWORD );
6067
private ByteArrayOutputStream baos;
6168
private ConnectionConfig connectionConfig;
6269
private CliArgs cliArgs;
@@ -89,7 +96,7 @@ public void setup() {
8996

9097
private void ensureUser() throws Exception {
9198
if (majorVersion(shell.getServerVersion() ) >= 4) {
92-
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
99+
shell.execute(":use " + SYSTEM_DB_NAME);
93100
shell.execute("CREATE OR REPLACE USER foo SET PASSWORD 'pass';");
94101
shell.execute("GRANT ROLE reader TO foo;");
95102
shell.execute(":use");
@@ -134,41 +141,23 @@ public void promptsOnWrongAuthenticationIfInteractive() throws Exception {
134141

135142
@Test
136143
public void promptsOnPasswordChangeRequired() throws Exception {
137-
shell.setCommandHelper(new CommandHelper(mock(Logger.class), Historian.empty, shell));
138-
inputBuffer.put(String.format("foo%npass%nnewpass%n").getBytes());
139-
140-
assertEquals("", connectionConfig.username());
141-
assertEquals("", connectionConfig.password());
142-
143-
// when
144-
main.connectMaybeInteractively(shell, connectionConfig, true, true);
145-
146-
// then
147-
// should be connected
148-
assertTrue(shell.isConnected());
149-
// should have prompted and set the username and password
150-
String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" );
151-
assertEquals(expectedLoginOutput, baos.toString());
152-
assertEquals("neo4j", connectionConfig.username());
153-
assertEquals("neo", connectionConfig.password());
154-
155-
// Create a new user
156-
ensureUser();
157-
shell.disconnect();
144+
int majorVersion = getVersionAndCreateUserWithPasswordChangeRequired();
158145

159146
connectionConfig = getConnectionConfig(cliArgs);
160147
assertEquals("", connectionConfig.username());
161148
assertEquals("", connectionConfig.password());
162149

163150
// when
151+
inputBuffer.put(String.format("foo%npass%nnewpass%n").getBytes());
152+
baos.reset();
164153
main.connectMaybeInteractively(shell, connectionConfig, true, true);
165154

166155
// then
167156
assertTrue(shell.isConnected());
168-
if (majorVersion(shell.getServerVersion() ) >= 4) {
157+
if (majorVersion >= 4) {
169158
// should have prompted to change the password
170159
String expectedChangePasswordOutput = format( "username: foo%npassword: ****%nPassword change required%nnew password: *******%n" );
171-
assertEquals(expectedLoginOutput + expectedChangePasswordOutput, baos.toString());
160+
assertEquals( expectedChangePasswordOutput, baos.toString());
172161
assertEquals("foo", connectionConfig.username());
173162
assertEquals("newpass", connectionConfig.password());
174163
assertNull(connectionConfig.newPassword());
@@ -178,7 +167,7 @@ public void promptsOnPasswordChangeRequired() throws Exception {
178167
} else {
179168
// in 3.x we do not get credentials expired exception on connection, but when we try to access data
180169
String expectedChangePasswordOutput = format( "username: foo%npassword: ****%n" );
181-
assertEquals(expectedLoginOutput + expectedChangePasswordOutput, baos.toString());
170+
assertEquals( expectedChangePasswordOutput, baos.toString());
182171
assertEquals("foo", connectionConfig.username());
183172
assertEquals("pass", connectionConfig.password());
184173

@@ -189,6 +178,61 @@ public void promptsOnPasswordChangeRequired() throws Exception {
189178
}
190179
}
191180

181+
@Test
182+
public void allowUserToUpdateExpiredPasswordInteractivelyWithoutBeingPrompted() throws Exception {
183+
//given a user that require a password change
184+
int majorVersion = getVersionAndCreateUserWithPasswordChangeRequired();
185+
186+
//when the user attempts a non-interactive password update
187+
assumeTrue(majorVersion >= 4 );
188+
baos.reset();
189+
assertEquals( EXIT_SUCCESS, main.runShell( args( SYSTEM_DB_NAME, "foo", "pass",
190+
"ALTER CURRENT USER SET PASSWORD from \"pass\" to \"pass2\";" ), shell, mock( Logger.class ) ) );
191+
//we shouldn't ask for a new password
192+
assertEquals( "", baos.toString() );
193+
194+
//then the new user should be able to successfully connect, and run a command
195+
assertEquals( format( "n%n42%n" ),
196+
executeNonInteractively( args( DEFAULT_DEFAULT_DB_NAME,
197+
"foo", "pass2", "RETURN 42 AS n" ) ) );
198+
}
199+
200+
@Test
201+
public void shouldFailIfNonInteractivelySettingPasswordOnNonSystemDb() throws Exception {
202+
//given a user that require a password change
203+
int majorVersion = getVersionAndCreateUserWithPasswordChangeRequired();
204+
205+
//when
206+
assumeTrue( majorVersion >= 4 );
207+
208+
//then
209+
assertEquals( EXIT_FAILURE, main.runShell( args( DEFAULT_DEFAULT_DB_NAME, "foo", "pass",
210+
"ALTER CURRENT USER SET PASSWORD from \"pass\" to \"pass2\";" ), shell, mock( Logger.class ) ) );
211+
}
212+
213+
@Test
214+
public void shouldBePromptedIfRunningNonInteractiveCypherThatDoesntUpdatePassword() throws Exception {
215+
//given a user that require a password change
216+
int majorVersion = getVersionAndCreateUserWithPasswordChangeRequired();
217+
218+
//when
219+
assumeTrue( majorVersion >= 4 );
220+
221+
//when interactively asked for a password use this
222+
inputBuffer.put( String.format( "pass2%n" ).getBytes() );
223+
baos.reset();
224+
assertEquals( EXIT_SUCCESS, main.runShell( args( DEFAULT_DEFAULT_DB_NAME, "foo", "pass",
225+
"MATCH (n) RETURN n" ), shell, mock( Logger.class ) ) );
226+
227+
//then should ask for a new password
228+
assertEquals( format( "Password change required%nnew password: *****%n" ), baos.toString() );
229+
230+
//then the new user should be able to successfully connect, and run a command
231+
assertEquals( format( "n%n42%n" ),
232+
executeNonInteractively( args( DEFAULT_DEFAULT_DB_NAME,
233+
"foo", "pass2", "RETURN 42 AS n" ) ) );
234+
}
235+
192236
@Test
193237
public void doesNotPromptToStdOutOnWrongAuthenticationIfOutputRedirected() throws Exception {
194238
// when
@@ -296,10 +340,15 @@ public void shouldReadMultipleCypherStatementsFromFile() throws Exception {
296340

297341
@Test
298342
public void shouldFailIfInputFileDoesntExist() throws Exception {
299-
// expect
300-
exception.expect( FileNotFoundException.class);
301-
exception.expectMessage( "what.cypher (No such file or directory)" );
302-
executeFileNonInteractively("what.cypher");
343+
//given
344+
ByteArrayOutputStream out = new ByteArrayOutputStream();
345+
Logger logger = new AnsiLogger( false, Format.VERBOSE, new PrintStream( out ), new PrintStream( out ));
346+
347+
//when
348+
executeFileNonInteractively("what.cypher", logger);
349+
350+
//then
351+
assertEquals( format("what.cypher (No such file or directory)%n"), out.toString());
303352
}
304353

305354
@Test
@@ -411,7 +460,7 @@ public void doesNotStartWhenDefaultDatabaseUnavailableIfInteractive() throws Exc
411460
assertEquals("neo", connectionConfig.password());
412461

413462
// Stop the default database
414-
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
463+
shell.execute(":use " + SYSTEM_DB_NAME);
415464
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);
416465

417466
try {
@@ -453,7 +502,7 @@ public void startsAgainstSystemDatabaseWhenDefaultDatabaseUnavailableIfInteracti
453502
assertEquals("neo", connectionConfig.password());
454503

455504
// Stop the default database
456-
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
505+
shell.execute(":use " + SYSTEM_DB_NAME);
457506
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);
458507

459508
try {
@@ -502,7 +551,7 @@ public void switchingToUnavailableDatabaseIfInteractive() throws Exception {
502551
assertEquals("neo", connectionConfig.password());
503552

504553
// Stop the default database
505-
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
554+
shell.execute(":use " + SYSTEM_DB_NAME);
506555
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);
507556

508557
try {
@@ -540,7 +589,7 @@ public void switchingToUnavailableDefaultDatabaseIfInteractive() throws Exceptio
540589
assertEquals("neo", connectionConfig.password());
541590

542591
// Stop the default database
543-
shell.execute(":use " + DatabaseManager.SYSTEM_DB_NAME);
592+
shell.execute(":use " + SYSTEM_DB_NAME);
544593
shell.execute("STOP DATABASE " + DatabaseManager.DEFAULT_DEFAULT_DB_NAME);
545594

546595
try {
@@ -554,23 +603,29 @@ public void switchingToUnavailableDefaultDatabaseIfInteractive() throws Exceptio
554603
}
555604
}
556605

557-
private String executeFileNonInteractively(String filename) throws Exception {
606+
private String executeFileNonInteractively(String filename) {
558607
return executeFileNonInteractively(filename, mock(Logger.class));
559608
}
560609

561-
private String executeFileNonInteractively(String filename, Logger logger) throws Exception
562-
{
610+
private String executeFileNonInteractively(String filename, Logger logger) {
563611
CliArgs cliArgs = new CliArgs();
612+
cliArgs.setUsername( USER, "" );
613+
cliArgs.setPassword( PASSWORD, "" );
564614
cliArgs.setInputFilename(filename);
565615

616+
return executeNonInteractively( cliArgs, logger );
617+
}
618+
619+
private String executeNonInteractively(CliArgs cliArgs) {
620+
return executeNonInteractively(cliArgs, mock(Logger.class));
621+
}
622+
623+
private String executeNonInteractively(CliArgs cliArgs, Logger logger)
624+
{
566625
ToStringLinePrinter linePrinter = new ToStringLinePrinter();
567626
ShellAndConnection sac = getShell( cliArgs, linePrinter );
568627
CypherShell shell = sac.shell;
569-
ConnectionConfig connectionConfig = sac.connectionConfig;
570-
main.connectMaybeInteractively( shell, connectionConfig, true, true );
571-
ShellRunner shellRunner = ShellRunner.getShellRunner(cliArgs, shell, logger, connectionConfig);
572-
shellRunner.runUntilEnd();
573-
628+
main.runShell(cliArgs, shell, logger);
574629
return linePrinter.result();
575630
}
576631

@@ -626,4 +681,26 @@ private void exit( CypherShell shell ) throws CommandException
626681
//do nothing
627682
}
628683
}
684+
685+
private CliArgs args(String db, String user, String pass, String cypher)
686+
{
687+
CliArgs cliArgs = new CliArgs();
688+
cliArgs.setUsername( user, "" );
689+
cliArgs.setPassword( pass, "" );
690+
cliArgs.setDatabase( db );
691+
cliArgs.setCypher( cypher );
692+
return cliArgs;
693+
}
694+
695+
private int getVersionAndCreateUserWithPasswordChangeRequired() throws Exception {
696+
shell.setCommandHelper( new CommandHelper( mock( Logger.class ), Historian.empty, shell ) );
697+
698+
main.connectMaybeInteractively( shell, connectionConfig, true, true );
699+
String expectedLoginOutput = format( "username: neo4j%npassword: ***%n" );
700+
assertEquals( expectedLoginOutput, baos.toString() );
701+
ensureUser();
702+
int majorVersion = majorVersion( shell.getServerVersion() );
703+
shell.disconnect();
704+
return majorVersion;
705+
}
629706
}

cypher-shell/src/main/java/org/neo4j/shell/ConnectionConfig.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ public void setPassword(@Nonnull String password) {
9595
this.password = password;
9696
}
9797

98-
public void setNewPassword(@Nonnull String password) {
98+
public void setNewPassword( String password) {
9999
this.newPassword = password;
100100
}
101101

cypher-shell/src/main/java/org/neo4j/shell/Connector.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package org.neo4j.shell;
22

3-
import org.neo4j.shell.exception.CommandException;
4-
53
import javax.annotation.Nonnull;
64

5+
import org.neo4j.function.ThrowingAction;
6+
import org.neo4j.shell.exception.CommandException;
7+
78
/**
89
* An object with the ability to connect and disconnect.
910
*/
@@ -18,7 +19,15 @@ public interface Connector {
1819
*
1920
* @throws CommandException if connection failed
2021
*/
21-
void connect(@Nonnull ConnectionConfig connectionConfig) throws CommandException;
22+
default void connect(@Nonnull ConnectionConfig connectionConfig) throws CommandException {
23+
connect( connectionConfig, null );
24+
}
25+
26+
/**
27+
*
28+
* @throws CommandException if connection failed
29+
*/
30+
void connect( @Nonnull ConnectionConfig connectionConfig, ThrowingAction<CommandException> action) throws CommandException;
2231

2332
/**
2433
* Returns the version of Neo4j which the shell is connected to. If the version is before 3.1.0-M09, or we are not

cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package org.neo4j.shell;
22

3+
import java.util.List;
4+
import java.util.Optional;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
7+
import javax.annotation.Nonnull;
8+
39
import org.neo4j.driver.exceptions.DiscoveryException;
410
import org.neo4j.driver.exceptions.Neo4jException;
511
import org.neo4j.driver.exceptions.ServiceUnavailableException;
12+
import org.neo4j.function.ThrowingAction;
613
import org.neo4j.shell.commands.Command;
714
import org.neo4j.shell.commands.CommandExecutable;
815
import org.neo4j.shell.commands.CommandHelper;
@@ -14,12 +21,6 @@
1421
import org.neo4j.shell.state.BoltResult;
1522
import org.neo4j.shell.state.BoltStateHandler;
1623

17-
import javax.annotation.Nonnull;
18-
import java.util.List;
19-
import java.util.Optional;
20-
import java.util.regex.Matcher;
21-
import java.util.regex.Pattern;
22-
2324
/**
2425
* A possibly interactive shell for evaluating cypher statements.
2526
*/
@@ -137,10 +138,12 @@ protected void executeCmd(@Nonnull final CommandExecutable cmdExe) throws ExitEx
137138
* Open a session to Neo4j
138139
*
139140
* @param connectionConfig
141+
* @param command
140142
*/
141143
@Override
142-
public void connect(@Nonnull ConnectionConfig connectionConfig) throws CommandException {
143-
boltStateHandler.connect(connectionConfig);
144+
public void connect( @Nonnull ConnectionConfig connectionConfig,
145+
ThrowingAction<CommandException> command) throws CommandException {
146+
boltStateHandler.connect(connectionConfig, command );
144147
}
145148

146149
@Nonnull

0 commit comments

Comments
 (0)