diff --git a/spring-boot-project/spring-boot-cli/pom.xml b/spring-boot-project/spring-boot-cli/pom.xml
index 5747c970e6c..013ea825a4e 100644
--- a/spring-boot-project/spring-boot-cli/pom.xml
+++ b/spring-boot-project/spring-boot-cli/pom.xml
@@ -43,6 +43,10 @@
org.springframework
spring-core
+
+ org.springframework.security
+ spring-security-crypto
+
org.apache.maven
maven-aether-provider
diff --git a/spring-boot-project/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java b/spring-boot-project/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java
index 4a5949ee38c..50d374ee19f 100644
--- a/spring-boot-project/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java
+++ b/spring-boot-project/spring-boot-cli/src/it/java/org/springframework/boot/cli/CommandLineIT.java
@@ -45,7 +45,7 @@ public class CommandLineIT {
assertThat(cli.await(), equalTo(0));
assertThat("Unexpected error: \n" + cli.getErrorOutput(),
cli.getErrorOutput().length(), equalTo(0));
- assertThat(cli.getStandardOutputLines().size(), equalTo(10));
+ assertThat(cli.getStandardOutputLines().size(), equalTo(11));
}
@Test
diff --git a/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java
index 0c557740cd1..3d76b3cd03d 100644
--- a/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java
+++ b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/DefaultCommandFactory.java
@@ -26,6 +26,7 @@ import org.springframework.boot.cli.command.CommandFactory;
import org.springframework.boot.cli.command.archive.JarCommand;
import org.springframework.boot.cli.command.archive.WarCommand;
import org.springframework.boot.cli.command.core.VersionCommand;
+import org.springframework.boot.cli.command.encodepassword.EncodePasswordCommand;
import org.springframework.boot.cli.command.grab.GrabCommand;
import org.springframework.boot.cli.command.init.InitCommand;
import org.springframework.boot.cli.command.install.InstallCommand;
@@ -51,6 +52,7 @@ public class DefaultCommandFactory implements CommandFactory {
defaultCommands.add(new InstallCommand());
defaultCommands.add(new UninstallCommand());
defaultCommands.add(new InitCommand());
+ defaultCommands.add(new EncodePasswordCommand());
DEFAULT_COMMANDS = Collections.unmodifiableList(defaultCommands);
}
diff --git a/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/encodepassword/EncodePasswordCommand.java b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/encodepassword/EncodePasswordCommand.java
new file mode 100644
index 00000000000..96a92893504
--- /dev/null
+++ b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/encodepassword/EncodePasswordCommand.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2012-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.springframework.boot.cli.command.encodepassword;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import joptsimple.OptionSet;
+import joptsimple.OptionSpec;
+
+import org.springframework.boot.cli.command.Command;
+import org.springframework.boot.cli.command.HelpExample;
+import org.springframework.boot.cli.command.OptionParsingCommand;
+import org.springframework.boot.cli.command.options.OptionHandler;
+import org.springframework.boot.cli.command.status.ExitStatus;
+import org.springframework.boot.cli.util.Log;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Command} to encode passwords for use with Spring Security.
+ *
+ * @author Phillip Webb
+ * @since 2.0.0
+ */
+public class EncodePasswordCommand extends OptionParsingCommand {
+
+ private static Map> ENCODERS;
+
+ static {
+ Map> encoders = new LinkedHashMap<>();
+ encoders.put("bcrypt", BCryptPasswordEncoder::new);
+ encoders.put("pbkdf2", Pbkdf2PasswordEncoder::new);
+ ENCODERS = Collections.unmodifiableMap(encoders);
+ }
+
+ public EncodePasswordCommand() {
+ super("encodepassword", "Encode a password for use with Spring Security",
+ new EncodePasswordOptionHandler());
+ }
+
+ @Override
+ public String getUsageHelp() {
+ return "[options] ";
+ }
+
+ @Override
+ public Collection getExamples() {
+ List examples = new ArrayList<>();
+ examples.add(new HelpExample("To encode a password with bcrypt",
+ "spring encodepassword mypassword"));
+ examples.add(new HelpExample("To encode a password with pbkdf2",
+ "spring encodepassword -a pbkdf2 mypassword"));
+ return examples;
+ }
+
+ private static final class EncodePasswordOptionHandler extends OptionHandler {
+
+ private OptionSpec algorithm;
+
+ @Override
+ protected void options() {
+ this.algorithm = option(Arrays.asList("algorithm", "a"),
+ "The algorithm to use").withRequiredArg().defaultsTo("bcrypt");
+ }
+
+ @Override
+ protected ExitStatus run(OptionSet options) throws Exception {
+ if (options.nonOptionArguments().size() != 1) {
+ Log.error("A single password option must be provided");
+ return ExitStatus.ERROR;
+ }
+ String algorithm = options.valueOf(this.algorithm);
+ String password = (String) options.nonOptionArguments().get(0);
+ Supplier encoder = ENCODERS.get(algorithm);
+ if (encoder == null) {
+ Log.error("Unknown algorithm, valid options are: " + StringUtils
+ .collectionToCommaDelimitedString(ENCODERS.keySet()));
+ return ExitStatus.ERROR;
+ }
+ Log.info(encoder.get().encode(password));
+ return ExitStatus.OK;
+ }
+
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/Log.java b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/Log.java
index 4982f7f77c7..730118f5d27 100644
--- a/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/Log.java
+++ b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/Log.java
@@ -23,20 +23,38 @@ package org.springframework.boot.cli.util;
*/
public abstract class Log {
+ private static LogListener listener;
+
public static void info(String message) {
System.out.println(message);
+ if (listener != null) {
+ listener.info(message);
+ }
}
public static void infoPrint(String message) {
System.out.print(message);
+ if (listener != null) {
+ listener.infoPrint(message);
+ }
}
public static void error(String message) {
System.err.println(message);
+ if (listener != null) {
+ listener.error(message);
+ }
}
public static void error(Exception ex) {
ex.printStackTrace(System.err);
+ if (listener != null) {
+ listener.error(ex);
+ }
+ }
+
+ static void setListener(LogListener listener) {
+ Log.listener = listener;
}
}
diff --git a/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/LogListener.java b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/LogListener.java
new file mode 100644
index 00000000000..7fdd17c83ea
--- /dev/null
+++ b/spring-boot-project/spring-boot-cli/src/main/java/org/springframework/boot/cli/util/LogListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2012-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.springframework.boot.cli.util;
+
+/**
+ * Listener that can be attached to the {@link Log} to capture calls.
+ *
+ * @author Phillip Webb
+ */
+interface LogListener {
+
+ void info(String message);
+
+ void infoPrint(String message);
+
+ void error(String message);
+
+ void error(Exception ex);
+
+}
diff --git a/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/encodepassword/EncodePasswordCommandTests.java b/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/encodepassword/EncodePasswordCommandTests.java
new file mode 100644
index 00000000000..8e1c5e78993
--- /dev/null
+++ b/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/command/encodepassword/EncodePasswordCommandTests.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.springframework.boot.cli.command.encodepassword;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.MockitoAnnotations;
+
+import org.springframework.boot.cli.command.status.ExitStatus;
+import org.springframework.boot.cli.util.MockLog;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link EncodePasswordCommand}.
+ *
+ * @author Phillip Webb
+ */
+public class EncodePasswordCommandTests {
+
+ private MockLog log;
+
+ @Captor
+ private ArgumentCaptor message;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ this.log = MockLog.attach();
+ }
+
+ @After
+ public void cleanup() {
+ MockLog.clear();
+ }
+
+ @Test
+ public void encodeWithNoAlgorithShouldUseBcrypt() throws Exception {
+ EncodePasswordCommand command = new EncodePasswordCommand();
+ ExitStatus status = command.run("boot");
+ verify(this.log).info(this.message.capture());
+ assertThat(new BCryptPasswordEncoder().matches("boot", this.message.getValue()))
+ .isTrue();
+ assertThat(status).isEqualTo(ExitStatus.OK);
+ }
+
+ @Test
+ public void encodeWithPbkdf2ShouldUsePbkdf2() throws Exception {
+ EncodePasswordCommand command = new EncodePasswordCommand();
+ ExitStatus status = command.run("-a", "pbkdf2", "boot");
+ verify(this.log).info(this.message.capture());
+ assertThat(new Pbkdf2PasswordEncoder().matches("boot", this.message.getValue()))
+ .isTrue();
+ assertThat(status).isEqualTo(ExitStatus.OK);
+ }
+
+ @Test
+ public void encodeWithUnkownAlgorithShouldExitWithError() throws Exception {
+ EncodePasswordCommand command = new EncodePasswordCommand();
+ ExitStatus status = command.run("--algorithm", "bad", "boot");
+ verify(this.log).error("Unknown algorithm, valid options are: bcrypt,pbkdf2");
+ assertThat(status).isEqualTo(ExitStatus.ERROR);
+ }
+
+}
diff --git a/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/util/MockLog.java b/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/util/MockLog.java
new file mode 100644
index 00000000000..9eeaa9d11da
--- /dev/null
+++ b/spring-boot-project/spring-boot-cli/src/test/java/org/springframework/boot/cli/util/MockLog.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.springframework.boot.cli.util;
+
+import static org.mockito.Mockito.mock;
+
+/**
+ * Mock log for testing message output.
+ *
+ * @author Phillip Webb
+ */
+public interface MockLog extends LogListener {
+
+ static MockLog attach() {
+ MockLog log = mock(MockLog.class);
+ Log.setListener(log);
+ return log;
+ }
+
+ static void clear() {
+ Log.setListener(null);
+ }
+
+}