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); + } + +}