Skip to content

Commit

Permalink
IGNITE-23459 Add console input if argument presented without value fo…
Browse files Browse the repository at this point in the history
…r ./control.sh
  • Loading branch information
Положаев Денис Александрович committed Nov 22, 2024
1 parent ffa6850 commit a459aa0
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@
import java.lang.reflect.Field;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
Expand Down Expand Up @@ -158,6 +161,9 @@ public class ArgumentParser {
/** */
private final List<CLIArgument<?>> common = new ArrayList<>();

/** Console instance. Public access needs for tests. */
protected final GridConsole console;

static {
SENSITIVE_ARGUMENTS.add(CMD_PASSWORD);
SENSITIVE_ARGUMENTS.add(CMD_KEYSTORE_PASSWORD);
Expand All @@ -177,8 +183,18 @@ public static boolean isSensitiveArgument(String arg) {
* @param registry Supported commands.
*/
public ArgumentParser(IgniteLogger log, IgniteCommandRegistry registry) {
this(log, registry, null);
}

/**
* @param log Logger.
* @param registry Supported commands.
* @param console Supported commands.
*/
public ArgumentParser(IgniteLogger log, IgniteCommandRegistry registry, GridConsole console) {
this.log = log;
this.registry = registry;
this.console = console;

BiConsumer<String, ?> securityWarn = (name, val) -> log.info(String.format("Warning: %s is insecure. " +
"Whenever possible, use interactive prompt for password (just discard %s option).", name, name));
Expand Down Expand Up @@ -253,6 +269,10 @@ public <A extends IgniteDataTransferObject> ConnectionAndSslParameters<A> parseA

CLIArgumentParser parser = createArgumentParser();

List<String> reqArgs = new ArrayList<>(requestArgsFromConsole(args, parser));

args.addAll(reqArgs);

parser.parse(args.iterator());

A arg = (A)argument(
Expand Down Expand Up @@ -378,4 +398,107 @@ private CLIArgumentParser createArgumentParser() {

return new CLIArgumentParser(positionalArgs, namedArgs);
}

/**
* Find arguments and request values from console.
*
* @param args list of arguments.
* @param parser instance of parser for command line arguments.
* @return List of requested arguments with value
* @throws IllegalArgumentException In case arguments specified more than once.
*/
private List<String> requestArgsFromConsole(List<String> args, CLIArgumentParser parser) {
List<String> reqArgs = new ArrayList<>();
List<String> removalArgs = new ArrayList<>();
String curr = null;

for (String nextArg : args) {
if (curr == null || !curr.startsWith(NAME_PREFIX) || parser.argType(curr.toLowerCase()).isEmpty()) {
curr = nextArg;
continue;
}

if (Collections.frequency(args, curr) > 1)
throw new IllegalArgumentException("The " + curr + " argument specified more than once");

if (nextArg.startsWith(NAME_PREFIX) && !parser.argType(curr.toLowerCase()).get().equals(boolean.class))
readValue(reqArgs,removalArgs, curr, parser);

curr = nextArg;
}

if (curr.startsWith(NAME_PREFIX) && !parser.argType(curr.toLowerCase()).get().equals(boolean.class))
readValue(reqArgs,removalArgs, curr, parser);

args.removeAll(removalArgs);

return reqArgs;
}

/**
* Request argument value from console.
*
* @param reqArgs list of requested arguments with value.
* @param removalArgs list of arguments for remove from args.
* @param curr current requested argument.
* @param parser instance of parser for command line arguments.
* @throws IllegalArgumentException In case arguments type is empty or unsupported.
*/
private void readValue(List<String> reqArgs, List<String> removalArgs, String curr, CLIArgumentParser parser) {
Optional<?> argType = parser.argType(curr.toLowerCase());

if (argType.isEmpty())
throw new IllegalArgumentException("Empty type argument: " + curr);

if (curr.equals(CMD_PASSWORD)) {
removalArgs.add(curr);
reqArgs.add(curr);
reqArgs.add(new String(requestPasswordFromConsole(curr.substring(NAME_PREFIX.length()) + ": ")));
} else if (char[].class.equals(argType.get())) {
removalArgs.add(curr);
reqArgs.add(curr);
reqArgs.add(new String(requestPasswordFromConsole(curr.substring(NAME_PREFIX.length()) + ": ")));
} else if (String.class.equals(argType.get())
|| String[].class.equals(argType.get())
|| char[].class.equals(argType.get())
|| Integer.class.equals(argType.get())
|| Long.class.equals(argType.get())) {
removalArgs.add(curr);
reqArgs.add(curr);
reqArgs.add(requestDataFromConsole(curr.substring(NAME_PREFIX.length()) + ": "));
} else
throw new IllegalArgumentException("Unsupported type for " + curr + " argument: " + argType.get());
}

/**
* Requests password from console with message.
*
* @param msg Message.
* @return Password.
* @throws UnsupportedOperationException In case the console is unavailable.
*/
private char[] requestPasswordFromConsole(String msg) {
if (console == null)
throw new UnsupportedOperationException("Failed to securely read password (console is unavailable): " + msg);
else
return console.readPassword(msg);
}

/**
* Requests data from console with message.
*
* @param msg Message.
* @return Input data.
*/
private String requestDataFromConsole(String msg) {
if (console != null)
return console.readLine(msg);
else {
Scanner scanner = new Scanner(System.in);

log.info(msg);

return scanner.nextLine();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ public <A extends IgniteDataTransferObject> int execute(List<String> rawArgs) {

verbose = F.exist(rawArgs, CMD_VERBOSE::equalsIgnoreCase);

ConnectionAndSslParameters<A> args = new ArgumentParser(logger, registry).parseAndValidate(rawArgs);
ConnectionAndSslParameters<A> args = new ArgumentParser(logger, registry, console).parseAndValidate(rawArgs);

cmdName = toFormattedCommandName(args.cmdPath().peekLast().getClass()).toUpperCase();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.util.GridStringBuilder;
Expand Down Expand Up @@ -207,6 +208,16 @@ public String usage() {
return sb.toString();
}

/**
* Return optional argument type.
*
* @param name Argument name.
* @return Optional argument type.
*/
public Optional<?> argType(String name) {
return Optional.ofNullable(argConfiguration.get(name).type());
}

/** */
private String argNameForUsage(CLIArgument<?> arg) {
if (arg.optional())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,137 @@ public void testConnector() throws Exception {
assertContains(log, testOutput, "--keystore-password *****");
assertContains(log, testOutput, "--truststore-password *****");
}

/**
* Verify that the command work correctly when request starts with the --password argument
* without value that invoke console password input for user, and that it is requested only once.
*
* @throws Exception If failed.
*/
@Test
public void testInputKeyUserPwdOnlyOncePwdArgStart() throws Exception {
performTest(Arrays.asList(
"--password",
"--state",
"--user", login,
"--keystore", keyStorePath("connectorClient"),
"--keystore-password", keyStorePassword(),
"--truststore", keyStorePath("trustthree"),
"--truststore-password", keyStorePassword()));
}

/**
* Verify that the command work correctly when request contains the --password argument inside
* without value that invoke console password input for user, and that it is requested only once.
*
* @throws Exception If failed.
*/
@Test
public void testInputKeyUserPwdOnlyOncePwdArgMiddle() throws Exception {
performTest(Arrays.asList(
"--state",
"--user", login,
"--password",
"--keystore", keyStorePath("connectorClient"),
"--keystore-password", keyStorePassword(),
"--truststore", keyStorePath("trustthree"),
"--truststore-password", keyStorePassword()));
}

/**
* Verify that the command work correctly when request ends with the --password argument
* without value that invoke console password input for user, and that it is requested only once.
*
* @throws Exception If failed.
*/
@Test
public void testInputKeyUserPwdOnlyOncePwdArgEnd() throws Exception {
performTest(Arrays.asList(
"--state",
"--user", login,
"--keystore", keyStorePath("connectorClient"),
"--keystore-password", keyStorePassword(),
"--truststore", keyStorePath("trustthree"),
"--truststore-password", keyStorePassword(),
"--password"));
}

/**
* Perform the test with prepared List arguments
*
* @param args List of query arguments.
* @throws Exception If failed.
*/
private void performTest(List<String> args) throws Exception {
IgniteEx crd = startGrid();

crd.cluster().state(ACTIVE);

TestCommandHandler hnd = newCommandHandler();

AtomicInteger pwdCnt = new AtomicInteger();

((CommandHandler)GridTestUtils.getFieldValue(hnd, "hnd")).console = new NoopConsole() {
/** {@inheritDoc} */
@Override public char[] readPassword(String fmt, Object... args) {
pwdCnt.incrementAndGet();
log.info("PASSWORD: " + pwd);
return pwd.toCharArray();
}
};

int exitCode = hnd.execute(args);

assertEquals(EXIT_CODE_OK, exitCode);
assertEquals(1, pwdCnt.get());
}

/**
* Verify that the command work correctly when request few arguments
* without value that invoke console input.
*
* @throws Exception If failed.
*/
@Test
public void testInputKeyForFewRequestedArguments() throws Exception {
IgniteEx crd = startGrid();

crd.cluster().state(ACTIVE);

TestCommandHandler hnd = newCommandHandler();

AtomicInteger usrCnt = new AtomicInteger();
AtomicInteger pwdCnt = new AtomicInteger();

((CommandHandler)GridTestUtils.getFieldValue(hnd, "hnd")).console = new NoopConsole() {
/** {@inheritDoc} */
@Override public String readLine(String fmt, Object... args) {
usrCnt.incrementAndGet();
return login;
}

/** {@inheritDoc} */
@Override public char[] readPassword(String fmt, Object... args) {
pwdCnt.incrementAndGet();
if (pwdCnt.get() == 1) {
return keyStorePassword().toCharArray();
}
return pwd.toCharArray();
}
};

int exitCode = hnd.execute(Arrays.asList(
"--state",
"--user",
"--verbose",
"--keystore", keyStorePath("connectorClient"),
"--keystore-password",
"--truststore", keyStorePath("trustthree"),
"--truststore-password", keyStorePassword(),
"--password"));

assertEquals(EXIT_CODE_OK, exitCode);
assertEquals(1, usrCnt.get());
assertEquals(2, pwdCnt.get());
}
}

0 comments on commit a459aa0

Please sign in to comment.