mirror of https://github.com/hyperledger/besu
NC-2043: Toml authentication provider (#689)
* NC-2043: Toml authentication provider * NC-2043: CLI option to generate hashed password * Renaming test * Removing unnecessary TomlAuth interface Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>pull/2/head
parent
157b42ed19
commit
721c713fa8
@ -0,0 +1,154 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 ConsenSys AG. |
||||||
|
* |
||||||
|
* 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 tech.pegasys.pantheon.ethereum.jsonrpc.authentication; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.List; |
||||||
|
import java.util.stream.Collectors; |
||||||
|
|
||||||
|
import io.vertx.core.AsyncResult; |
||||||
|
import io.vertx.core.Future; |
||||||
|
import io.vertx.core.Handler; |
||||||
|
import io.vertx.core.Vertx; |
||||||
|
import io.vertx.core.json.JsonObject; |
||||||
|
import io.vertx.ext.auth.AuthProvider; |
||||||
|
import io.vertx.ext.auth.User; |
||||||
|
import net.consensys.cava.toml.Toml; |
||||||
|
import net.consensys.cava.toml.TomlParseResult; |
||||||
|
import net.consensys.cava.toml.TomlTable; |
||||||
|
import org.apache.logging.log4j.LogManager; |
||||||
|
import org.apache.logging.log4j.Logger; |
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt; |
||||||
|
|
||||||
|
public class TomlAuth implements AuthProvider { |
||||||
|
|
||||||
|
private static final Logger LOG = LogManager.getLogger(); |
||||||
|
private final Vertx vertx; |
||||||
|
private final TomlAuthOptions options; |
||||||
|
|
||||||
|
public TomlAuth(final Vertx vertx, final TomlAuthOptions options) { |
||||||
|
this.vertx = vertx; |
||||||
|
this.options = options; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void authenticate( |
||||||
|
final JsonObject authInfo, final Handler<AsyncResult<User>> resultHandler) { |
||||||
|
final String username = authInfo.getString("username"); |
||||||
|
if (username == null) { |
||||||
|
resultHandler.handle(Future.failedFuture("No username provided")); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
final String password = authInfo.getString("password"); |
||||||
|
if (password == null) { |
||||||
|
resultHandler.handle(Future.failedFuture("No password provided")); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
LOG.debug("Authenticating user {} with password {}", username, password); |
||||||
|
|
||||||
|
readUser( |
||||||
|
username, |
||||||
|
rs -> { |
||||||
|
if (rs.succeeded()) { |
||||||
|
TomlUser user = rs.result(); |
||||||
|
checkPasswordHash( |
||||||
|
password, |
||||||
|
user.getPassword(), |
||||||
|
rs2 -> { |
||||||
|
if (rs2.succeeded()) { |
||||||
|
resultHandler.handle(Future.succeededFuture(user)); |
||||||
|
} else { |
||||||
|
resultHandler.handle(Future.failedFuture(rs2.cause())); |
||||||
|
} |
||||||
|
}); |
||||||
|
} else { |
||||||
|
resultHandler.handle(Future.failedFuture(rs.cause())); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private void readUser(final String username, final Handler<AsyncResult<TomlUser>> resultHandler) { |
||||||
|
vertx.executeBlocking( |
||||||
|
f -> { |
||||||
|
TomlParseResult parseResult; |
||||||
|
try { |
||||||
|
parseResult = Toml.parse(options.getTomlPath()); |
||||||
|
} catch (IOException e) { |
||||||
|
f.fail(e); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
final TomlTable userData = parseResult.getTableOrEmpty("Users." + username); |
||||||
|
if (userData.isEmpty()) { |
||||||
|
f.fail("User not found"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
final TomlUser tomlUser = readTomlUserFromTable(username, userData); |
||||||
|
if ("".equals(tomlUser.getPassword())) { |
||||||
|
f.fail("No password set for user"); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
f.complete(tomlUser); |
||||||
|
}, |
||||||
|
res -> { |
||||||
|
if (res.succeeded()) { |
||||||
|
resultHandler.handle(Future.succeededFuture((TomlUser) res.result())); |
||||||
|
} else { |
||||||
|
resultHandler.handle(Future.failedFuture(res.cause())); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private TomlUser readTomlUserFromTable(final String username, final TomlTable userData) { |
||||||
|
final String saltedAndHashedPassword = userData.getString("password", () -> ""); |
||||||
|
final List<String> groups = |
||||||
|
userData |
||||||
|
.getArrayOrEmpty("groups") |
||||||
|
.toList() |
||||||
|
.stream() |
||||||
|
.map(Object::toString) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
final List<String> permissions = |
||||||
|
userData |
||||||
|
.getArrayOrEmpty("permissions") |
||||||
|
.toList() |
||||||
|
.stream() |
||||||
|
.map(Object::toString) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
final List<String> roles = |
||||||
|
userData |
||||||
|
.getArrayOrEmpty("roles") |
||||||
|
.toList() |
||||||
|
.stream() |
||||||
|
.map(Object::toString) |
||||||
|
.collect(Collectors.toList()); |
||||||
|
|
||||||
|
return new TomlUser(username, saltedAndHashedPassword, groups, permissions, roles); |
||||||
|
} |
||||||
|
|
||||||
|
private void checkPasswordHash( |
||||||
|
final String password, |
||||||
|
final String passwordHash, |
||||||
|
final Handler<AsyncResult<Void>> resultHandler) { |
||||||
|
boolean passwordMatches = BCrypt.checkpw(password, passwordHash); |
||||||
|
if (passwordMatches) { |
||||||
|
resultHandler.handle(Future.succeededFuture()); |
||||||
|
} else { |
||||||
|
resultHandler.handle(Future.failedFuture("Invalid password")); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,50 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 ConsenSys AG. |
||||||
|
* |
||||||
|
* 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 tech.pegasys.pantheon.ethereum.jsonrpc.authentication; |
||||||
|
|
||||||
|
import java.nio.file.Path; |
||||||
|
import java.nio.file.Paths; |
||||||
|
|
||||||
|
import io.vertx.core.Vertx; |
||||||
|
import io.vertx.ext.auth.AuthOptions; |
||||||
|
import io.vertx.ext.auth.AuthProvider; |
||||||
|
|
||||||
|
public class TomlAuthOptions implements AuthOptions { |
||||||
|
|
||||||
|
private Path tomlPath; |
||||||
|
|
||||||
|
public TomlAuthOptions() {} |
||||||
|
|
||||||
|
public TomlAuthOptions(final TomlAuthOptions that) { |
||||||
|
tomlPath = that.tomlPath; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthProvider createProvider(final Vertx vertx) { |
||||||
|
return new TomlAuth(vertx, this); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public AuthOptions clone() { |
||||||
|
return new TomlAuthOptions(this); |
||||||
|
} |
||||||
|
|
||||||
|
public TomlAuthOptions setTomlPath(final String tomlPath) { |
||||||
|
this.tomlPath = Paths.get(tomlPath); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
public Path getTomlPath() { |
||||||
|
return tomlPath; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 ConsenSys AG. |
||||||
|
* |
||||||
|
* 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 tech.pegasys.pantheon.ethereum.jsonrpc.authentication; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import io.vertx.core.AsyncResult; |
||||||
|
import io.vertx.core.Handler; |
||||||
|
import io.vertx.core.json.JsonObject; |
||||||
|
import io.vertx.ext.auth.AbstractUser; |
||||||
|
import io.vertx.ext.auth.AuthProvider; |
||||||
|
|
||||||
|
public class TomlUser extends AbstractUser { |
||||||
|
|
||||||
|
private final String username; |
||||||
|
private final String password; |
||||||
|
private final List<String> groups; |
||||||
|
private final List<String> permissions; |
||||||
|
private final List<String> roles; |
||||||
|
|
||||||
|
TomlUser( |
||||||
|
final String username, |
||||||
|
final String password, |
||||||
|
final List<String> groups, |
||||||
|
final List<String> permissions, |
||||||
|
final List<String> roles) { |
||||||
|
this.username = username; |
||||||
|
this.password = password; |
||||||
|
this.groups = groups; |
||||||
|
this.permissions = permissions; |
||||||
|
this.roles = roles; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public JsonObject principal() { |
||||||
|
return new JsonObject() |
||||||
|
.put("username", username) |
||||||
|
.put("password", password) |
||||||
|
.put("groups", groups) |
||||||
|
.put("permissions", permissions) |
||||||
|
.put("roles", roles); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setAuthProvider(final AuthProvider authProvider) { |
||||||
|
// we only use Toml for authentication
|
||||||
|
throw new UnsupportedOperationException("Not implemented"); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void doIsPermitted( |
||||||
|
final String permission, final Handler<AsyncResult<Boolean>> resultHandler) { |
||||||
|
// we only use Toml for authentication
|
||||||
|
throw new UnsupportedOperationException("Not implemented"); |
||||||
|
} |
||||||
|
|
||||||
|
public String getUsername() { |
||||||
|
return username; |
||||||
|
} |
||||||
|
|
||||||
|
public String getPassword() { |
||||||
|
return password; |
||||||
|
} |
||||||
|
|
||||||
|
public List<String> getGroups() { |
||||||
|
return groups; |
||||||
|
} |
||||||
|
|
||||||
|
public List<String> getPermissions() { |
||||||
|
return permissions; |
||||||
|
} |
||||||
|
|
||||||
|
public List<String> getRoles() { |
||||||
|
return roles; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,128 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 ConsenSys AG. |
||||||
|
* |
||||||
|
* 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 tech.pegasys.pantheon.ethereum.jsonrpc.authentication; |
||||||
|
|
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.nio.file.NoSuchFileException; |
||||||
|
import java.nio.file.Paths; |
||||||
|
|
||||||
|
import io.vertx.core.Vertx; |
||||||
|
import io.vertx.core.json.JsonArray; |
||||||
|
import io.vertx.core.json.JsonObject; |
||||||
|
import io.vertx.ext.unit.TestContext; |
||||||
|
import io.vertx.ext.unit.junit.VertxUnitRunner; |
||||||
|
import org.junit.Before; |
||||||
|
import org.junit.Test; |
||||||
|
import org.junit.runner.RunWith; |
||||||
|
|
||||||
|
@RunWith(VertxUnitRunner.class) |
||||||
|
public class TomlAuthTest { |
||||||
|
|
||||||
|
private Vertx vertx; |
||||||
|
private JsonObject validAuthInfo; |
||||||
|
private TomlAuth tomlAuth; |
||||||
|
|
||||||
|
@Before |
||||||
|
public void before(final TestContext context) throws URISyntaxException { |
||||||
|
vertx = Vertx.vertx(); |
||||||
|
tomlAuth = |
||||||
|
new TomlAuth( |
||||||
|
vertx, new TomlAuthOptions().setTomlPath(getTomlPath("authentication/auth.toml"))); |
||||||
|
validAuthInfo = new JsonObject().put("username", "userA").put("password", "pegasys"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authInfoWithoutUsernameShouldFailAuthentication(final TestContext context) { |
||||||
|
JsonObject authInfo = new JsonObject().put("password", "foo"); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
authInfo, |
||||||
|
context.asyncAssertFailure( |
||||||
|
th -> context.assertEquals("No username provided", th.getMessage()))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authInfoWithoutPasswordShouldFailAuthentication(final TestContext context) { |
||||||
|
JsonObject authInfo = new JsonObject().put("username", "foo"); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
authInfo, |
||||||
|
context.asyncAssertFailure( |
||||||
|
th -> context.assertEquals("No password provided", th.getMessage()))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void parseFailureWithIOExceptionShouldFailAuthentication(final TestContext context) { |
||||||
|
tomlAuth = new TomlAuth(vertx, new TomlAuthOptions().setTomlPath("invalid_path")); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
validAuthInfo, |
||||||
|
context.asyncAssertFailure( |
||||||
|
th -> { |
||||||
|
context.assertEquals(th.getClass(), NoSuchFileException.class); |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void authInfoWithAbsentUserShouldFailAuthentication(final TestContext context) { |
||||||
|
JsonObject authInfo = new JsonObject().put("username", "foo").put("password", "foo"); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
authInfo, |
||||||
|
context.asyncAssertFailure(th -> context.assertEquals("User not found", th.getMessage()))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void userWithoutPasswordSetShouldFailAuthentication(final TestContext context) { |
||||||
|
JsonObject authInfo = new JsonObject().put("username", "noPasswordUser").put("password", "foo"); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
authInfo, |
||||||
|
context.asyncAssertFailure( |
||||||
|
th -> context.assertEquals("No password set for user", th.getMessage()))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void passwordMismatchShouldFailAuthentication(final TestContext context) { |
||||||
|
JsonObject authInfo = new JsonObject().put("username", "userA").put("password", "foo"); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
authInfo, |
||||||
|
context.asyncAssertFailure( |
||||||
|
th -> context.assertEquals("Invalid password", th.getMessage()))); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void validPasswordShouldAuthenticateSuccessfully(final TestContext context) { |
||||||
|
JsonObject expectedPrincipal = |
||||||
|
new JsonObject() |
||||||
|
.put("username", "userA") |
||||||
|
.put("password", "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC") |
||||||
|
.put("groups", new JsonArray().add("admin")) |
||||||
|
.put("permissions", new JsonArray().add("eth:*").add("perm:*")) |
||||||
|
.put("roles", new JsonArray().add("net")); |
||||||
|
|
||||||
|
JsonObject authInfo = new JsonObject().put("username", "userA").put("password", "pegasys"); |
||||||
|
|
||||||
|
tomlAuth.authenticate( |
||||||
|
authInfo, |
||||||
|
context.asyncAssertSuccess( |
||||||
|
res -> context.assertEquals(expectedPrincipal, res.principal()))); |
||||||
|
} |
||||||
|
|
||||||
|
private String getTomlPath(final String tomlFileName) throws URISyntaxException { |
||||||
|
return Paths.get(ClassLoader.getSystemResource(tomlFileName).toURI()) |
||||||
|
.toAbsolutePath() |
||||||
|
.toString(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
[Users.userA] |
||||||
|
password = "$2a$10$l3GA7K8g6rJ/Yv.YFSygCuI9byngpEzxgWS9qEg5emYDZomQW7fGC" |
||||||
|
groups = ["admin"] |
||||||
|
permissions = ["eth:*", "perm:*"] |
||||||
|
roles = ["net"] |
||||||
|
|
||||||
|
[Groups.admins] |
||||||
|
roles = ["admin"] |
||||||
|
|
||||||
|
[Roles.admin] |
||||||
|
permissions = ["admin:*"] |
||||||
|
|
||||||
|
[Roles.net] |
||||||
|
permissions = ["net:*"] |
||||||
|
|
||||||
|
[Users.noPasswordUser] |
||||||
|
groups = ["admin"] |
@ -0,0 +1,57 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2018 ConsenSys AG. |
||||||
|
* |
||||||
|
* 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 tech.pegasys.pantheon.cli; |
||||||
|
|
||||||
|
import static tech.pegasys.pantheon.cli.PasswordSubCommand.COMMAND_NAME; |
||||||
|
|
||||||
|
import java.io.PrintStream; |
||||||
|
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCrypt; |
||||||
|
import picocli.CommandLine.Command; |
||||||
|
import picocli.CommandLine.Model.CommandSpec; |
||||||
|
import picocli.CommandLine.Parameters; |
||||||
|
import picocli.CommandLine.ParentCommand; |
||||||
|
import picocli.CommandLine.Spec; |
||||||
|
|
||||||
|
@Command( |
||||||
|
name = COMMAND_NAME, |
||||||
|
description = "This command generates the hash of a given password.", |
||||||
|
mixinStandardHelpOptions = true |
||||||
|
) |
||||||
|
class PasswordSubCommand implements Runnable { |
||||||
|
|
||||||
|
static final String COMMAND_NAME = "password-hash"; |
||||||
|
|
||||||
|
@SuppressWarnings("unused") |
||||||
|
@ParentCommand |
||||||
|
private PantheonCommand parentCommand; // Picocli injects reference to parent command
|
||||||
|
|
||||||
|
@SuppressWarnings("unused") |
||||||
|
@Spec |
||||||
|
private CommandSpec spec; // Picocli injects reference to command spec
|
||||||
|
|
||||||
|
final PrintStream out; |
||||||
|
|
||||||
|
@SuppressWarnings("FieldMustBeFinal") |
||||||
|
@Parameters(arity = "1..1", description = "The password input") |
||||||
|
private String password = null; |
||||||
|
|
||||||
|
PasswordSubCommand(final PrintStream out) { |
||||||
|
this.out = out; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void run() { |
||||||
|
out.print(BCrypt.hashpw(password, BCrypt.gensalt())); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2019 ConsenSys AG. |
||||||
|
* |
||||||
|
* 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 tech.pegasys.pantheon.cli; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
import picocli.CommandLine.Model.CommandSpec; |
||||||
|
|
||||||
|
public class PasswordSubCommandTest extends CommandTestAbstract { |
||||||
|
|
||||||
|
@Test |
||||||
|
public void passwordHashSubCommandExists() { |
||||||
|
CommandSpec spec = parseCommand(); |
||||||
|
assertThat(spec.subcommands()).containsKeys("password-hash"); |
||||||
|
assertThat(commandOutput.toString()).isEmpty(); |
||||||
|
assertThat(commandErrorOutput.toString()).isEmpty(); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void callingPasswordHashWithoutPasswordParameterMustDisplayUsage() { |
||||||
|
final String expectedUsage = |
||||||
|
"Missing required parameter: <password>" |
||||||
|
+ System.lineSeparator() |
||||||
|
+ "Usage: pantheon password-hash [-hV] <password>" |
||||||
|
+ System.lineSeparator() |
||||||
|
+ "This command generates the hash of a given password." |
||||||
|
+ System.lineSeparator() |
||||||
|
+ " <password> The password input" |
||||||
|
+ System.lineSeparator() |
||||||
|
+ " -h, --help Show this help message and exit." |
||||||
|
+ System.lineSeparator() |
||||||
|
+ " -V, --version Print version information and exit." |
||||||
|
+ System.lineSeparator(); |
||||||
|
|
||||||
|
parseCommand("password-hash"); |
||||||
|
assertThat(commandOutput.toString()).isEmpty(); |
||||||
|
assertThat(commandErrorOutput.toString()).startsWith(expectedUsage); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void publicKeySubCommandExistAnbHaveSubCommands() { |
||||||
|
parseCommand("password-hash", "foo"); |
||||||
|
|
||||||
|
// we can't predict the final value so we are only checking if it starts with the hash marker
|
||||||
|
assertThat(commandOutput.toString()).startsWith("$2"); |
||||||
|
assertThat(commandErrorOutput.toString()).isEmpty(); |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue