Skip to content

Commit 2332899

Browse files
authored
IGNITE-23459 Added support for interactive input of sensitive command arguments in control utility (#11604)
1 parent 3db801d commit 2332899

File tree

14 files changed

+477
-209
lines changed

14 files changed

+477
-209
lines changed

modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/ArgumentParser.java

Lines changed: 82 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
* limitations under the License.
1616
*/
1717

18-
1918
package org.apache.ignite.internal.commandline;
2019

2120
import java.lang.reflect.Field;
2221
import java.util.ArrayDeque;
2322
import java.util.ArrayList;
2423
import java.util.Deque;
25-
import java.util.HashSet;
2624
import java.util.Iterator;
2725
import java.util.List;
26+
import java.util.ListIterator;
2827
import java.util.Set;
2928
import java.util.function.BiConsumer;
3029
import java.util.function.BiFunction;
@@ -42,15 +41,17 @@
4241
import org.apache.ignite.internal.management.api.CommandUtils;
4342
import org.apache.ignite.internal.management.api.CommandsRegistry;
4443
import org.apache.ignite.internal.management.api.Positional;
44+
import org.apache.ignite.internal.util.typedef.internal.SB;
4545
import org.apache.ignite.internal.util.typedef.internal.U;
4646
import org.apache.ignite.lang.IgniteExperimental;
47-
import org.apache.ignite.ssl.SslContextFactory;
4847

4948
import static org.apache.ignite.IgniteSystemProperties.IGNITE_ENABLE_EXPERIMENTAL_COMMAND;
5049
import static org.apache.ignite.internal.commandline.CommandHandler.DFLT_HOST;
5150
import static org.apache.ignite.internal.commandline.CommandHandler.DFLT_PORT;
5251
import static org.apache.ignite.internal.commandline.CommandHandler.UTILITY_NAME;
53-
import static org.apache.ignite.internal.commandline.argument.parser.CLIArgument.optionalArg;
52+
import static org.apache.ignite.internal.commandline.argument.parser.CLIArgument.CLIArgumentBuilder.argument;
53+
import static org.apache.ignite.internal.commandline.argument.parser.CLIArgument.CLIArgumentBuilder.optionalArgument;
54+
import static org.apache.ignite.internal.commandline.argument.parser.CLIArgumentParser.readNextValueToken;
5455
import static org.apache.ignite.internal.management.api.CommandUtils.CMD_WORDS_DELIM;
5556
import static org.apache.ignite.internal.management.api.CommandUtils.NAME_PREFIX;
5657
import static org.apache.ignite.internal.management.api.CommandUtils.PARAM_WORDS_DELIM;
@@ -62,7 +63,9 @@
6263
import static org.apache.ignite.internal.management.api.CommandUtils.toFormattedCommandName;
6364
import static org.apache.ignite.internal.management.api.CommandUtils.toFormattedFieldName;
6465
import static org.apache.ignite.internal.management.api.CommandUtils.visitCommandParams;
66+
import static org.apache.ignite.ssl.SslContextFactory.DFLT_KEY_ALGORITHM;
6567
import static org.apache.ignite.ssl.SslContextFactory.DFLT_SSL_PROTOCOL;
68+
import static org.apache.ignite.ssl.SslContextFactory.DFLT_STORE_TYPE;
6669

6770
/**
6871
* Argument parser.
@@ -131,9 +134,6 @@ public class ArgumentParser {
131134
/** */
132135
static final String CMD_SSL_FACTORY = "--ssl-factory";
133136

134-
/** Set of sensitive arguments */
135-
private static final Set<String> SENSITIVE_ARGUMENTS = new HashSet<>();
136-
137137
/** */
138138
private static final BiConsumer<String, Integer> PORT_VALIDATOR = (name, val) -> {
139139
if (val <= 0 || val > 65535)
@@ -143,64 +143,46 @@ public class ArgumentParser {
143143
/** */
144144
private final List<CLIArgument<?>> common = new ArrayList<>();
145145

146-
static {
147-
SENSITIVE_ARGUMENTS.add(CMD_PASSWORD);
148-
SENSITIVE_ARGUMENTS.add(CMD_KEYSTORE_PASSWORD);
149-
SENSITIVE_ARGUMENTS.add(CMD_TRUSTSTORE_PASSWORD);
150-
}
151-
152-
/**
153-
* @param arg To check.
154-
* @return True if provided argument is among sensitive one and not should be displayed.
155-
*/
156-
public static boolean isSensitiveArgument(String arg) {
157-
return SENSITIVE_ARGUMENTS.contains(arg);
158-
}
146+
/** Console instance. */
147+
protected final GridConsole console;
159148

160149
/**
161150
* @param log Logger.
162151
* @param registry Supported commands.
152+
* @param console Console.
163153
*/
164-
public ArgumentParser(IgniteLogger log, IgniteCommandRegistry registry) {
154+
public ArgumentParser(IgniteLogger log, IgniteCommandRegistry registry, GridConsole console) {
165155
this.log = log;
166156
this.registry = registry;
167-
168-
BiConsumer<String, ?> securityWarn = (name, val) -> log.info(String.format("Warning: %s is insecure. " +
169-
"Whenever possible, use interactive prompt for password (just discard %s option).", name, name));
170-
171-
arg(CMD_HOST, "HOST_OR_IP", String.class, DFLT_HOST);
172-
arg(CMD_PORT, "PORT", Integer.class, DFLT_PORT, PORT_VALIDATOR);
173-
arg(CMD_USER, "USER", String.class, null);
174-
arg(CMD_PASSWORD, "PASSWORD", String.class, null, (BiConsumer<String, String>)securityWarn);
175-
arg(CMD_VERBOSE, CMD_VERBOSE, boolean.class, false);
176-
arg(CMD_SSL_PROTOCOL, "SSL_PROTOCOL[, SSL_PROTOCOL_2, ..., SSL_PROTOCOL_N]", String[].class, new String[] {DFLT_SSL_PROTOCOL});
177-
arg(CMD_SSL_CIPHER_SUITES, "SSL_CIPHER_1[, SSL_CIPHER_2, ..., SSL_CIPHER_N]", String[].class, null);
178-
arg(CMD_SSL_KEY_ALGORITHM, "SSL_KEY_ALGORITHM", String.class, SslContextFactory.DFLT_KEY_ALGORITHM);
179-
arg(CMD_SSL_FACTORY, "SSL_FACTORY_PATH", String.class, null);
180-
arg(CMD_KEYSTORE_TYPE, "KEYSTORE_TYPE", String.class, SslContextFactory.DFLT_STORE_TYPE);
181-
arg(CMD_KEYSTORE, "KEYSTORE_PATH", String.class, null);
182-
arg(CMD_KEYSTORE_PASSWORD, "KEYSTORE_PASSWORD", char[].class, null, (BiConsumer<String, char[]>)securityWarn);
183-
arg(CMD_TRUSTSTORE_TYPE, "TRUSTSTORE_TYPE", String.class, SslContextFactory.DFLT_STORE_TYPE);
184-
arg(CMD_TRUSTSTORE, "TRUSTSTORE_PATH", String.class, null);
185-
arg(CMD_TRUSTSTORE_PASSWORD, "TRUSTSTORE_PASSWORD", char[].class, null, (BiConsumer<String, char[]>)securityWarn);
186-
arg(CMD_AUTO_CONFIRMATION, CMD_AUTO_CONFIRMATION, boolean.class, false);
187-
arg(
188-
CMD_ENABLE_EXPERIMENTAL,
189-
CMD_ENABLE_EXPERIMENTAL, Boolean.class,
190-
IgniteSystemProperties.getBoolean(IGNITE_ENABLE_EXPERIMENTAL_COMMAND)
157+
this.console = console;
158+
159+
common.addAll(List.of(
160+
optionalArgument(CMD_HOST, String.class).withUsage("HOST_OR_IP").withDefault(DFLT_HOST).build(),
161+
optionalArgument(CMD_PORT, Integer.class).withUsage("PORT").withDefault(DFLT_PORT).withValidator(PORT_VALIDATOR).build(),
162+
optionalArgument(CMD_USER, String.class).withUsage("USER").build(),
163+
optionalArgument(CMD_PASSWORD, String.class).withUsage("PASSWORD").markSensitive().build(),
164+
optionalArgument(CMD_VERBOSE, boolean.class).withUsage(CMD_VERBOSE).withDefault(false).build(),
165+
optionalArgument(CMD_SSL_PROTOCOL, String[].class)
166+
.withUsage("SSL_PROTOCOL[, SSL_PROTOCOL_2, ..., SSL_PROTOCOL_N]")
167+
.withDefault(t -> new String[] {DFLT_SSL_PROTOCOL})
168+
.build(),
169+
optionalArgument(CMD_SSL_CIPHER_SUITES, String[].class).withUsage("SSL_CIPHER_1[, SSL_CIPHER_2, ..., SSL_CIPHER_N]").build(),
170+
optionalArgument(CMD_SSL_KEY_ALGORITHM, String.class).withUsage("SSL_KEY_ALGORITHM").withDefault(DFLT_KEY_ALGORITHM).build(),
171+
optionalArgument(CMD_SSL_FACTORY, String.class).withUsage("SSL_FACTORY_PATH").build(),
172+
optionalArgument(CMD_KEYSTORE_TYPE, String.class).withUsage("KEYSTORE_TYPE").withDefault(DFLT_STORE_TYPE).build(),
173+
optionalArgument(CMD_KEYSTORE, String.class).withUsage("KEYSTORE_PATH").build(),
174+
optionalArgument(CMD_KEYSTORE_PASSWORD, char[].class).withUsage("KEYSTORE_PASSWORD").markSensitive().build(),
175+
optionalArgument(CMD_TRUSTSTORE_TYPE, String.class).withUsage("TRUSTSTORE_TYPE").withDefault(DFLT_STORE_TYPE).build(),
176+
optionalArgument(CMD_TRUSTSTORE, String.class).withUsage("TRUSTSTORE_PATH").build(),
177+
optionalArgument(CMD_TRUSTSTORE_PASSWORD, char[].class).withUsage("TRUSTSTORE_PASSWORD").markSensitive().build(),
178+
optionalArgument(CMD_AUTO_CONFIRMATION, boolean.class).withUsage(CMD_AUTO_CONFIRMATION).withDefault(false).build(),
179+
optionalArgument(CMD_ENABLE_EXPERIMENTAL, Boolean.class)
180+
.withUsage(CMD_ENABLE_EXPERIMENTAL)
181+
.withDefault(t -> IgniteSystemProperties.getBoolean(IGNITE_ENABLE_EXPERIMENTAL_COMMAND))
182+
.build())
191183
);
192184
}
193185

194-
/** */
195-
private <T> void arg(String name, String usage, Class<T> type, T dflt, BiConsumer<String, T> validator) {
196-
common.add(optionalArg(name, usage, type, t -> dflt, validator));
197-
}
198-
199-
/** */
200-
private <T> void arg(String name, String usage, Class<T> type, T dflt) {
201-
common.add(optionalArg(name, usage, type, () -> dflt));
202-
}
203-
204186
/**
205187
* Creates list of common utility options.
206188
*
@@ -236,7 +218,9 @@ public <A extends IgniteDataTransferObject> ConnectionAndSslParameters<A> parseA
236218

237219
CLIArgumentParser parser = createArgumentParser();
238220

239-
parser.parse(args.iterator());
221+
parser.parse(args.listIterator());
222+
223+
String safeCmd = buildSafeCommandString(raw.listIterator(), parser);
240224

241225
A arg = (A)argument(
242226
cmdPath.peek().argClass(),
@@ -253,7 +237,7 @@ public <A extends IgniteDataTransferObject> ConnectionAndSslParameters<A> parseA
253237
throw new IllegalArgumentException("Experimental commands disabled");
254238
}
255239

256-
return new ConnectionAndSslParameters<>(cmdPath, arg, parser);
240+
return new ConnectionAndSslParameters<>(cmdPath, arg, parser, safeCmd);
257241
}
258242

259243
/**
@@ -323,14 +307,11 @@ private CLIArgumentParser createArgumentParser() {
323307
List<CLIArgument<?>> positionalArgs = new ArrayList<>();
324308
List<CLIArgument<?>> namedArgs = new ArrayList<>();
325309

326-
BiFunction<Field, Boolean, CLIArgument<?>> toArg = (fld, optional) -> new CLIArgument<>(
327-
toFormattedFieldName(fld).toLowerCase(),
328-
null,
329-
optional,
330-
fld.getType(),
331-
null,
332-
(name, val) -> {}
333-
);
310+
BiFunction<Field, Boolean, CLIArgument<?>> toArg =
311+
(fld, optional) -> argument(toFormattedFieldName(fld).toLowerCase(), fld.getType())
312+
.withOptional(optional)
313+
.withSensitive(fld.getAnnotation(Argument.class).sensitive())
314+
.build();
334315

335316
List<Set<String>> grpdFlds = CommandUtils.argumentGroupsValues(cmdPath.peek().argClass());
336317

@@ -339,14 +320,10 @@ private CLIArgumentParser createArgumentParser() {
339320
|| fld.getAnnotation(Argument.class).optional())
340321
);
341322

342-
Consumer<Field> positionalArgCb = fld -> positionalArgs.add(new CLIArgument<>(
343-
fld.getName().toLowerCase(),
344-
null,
345-
fld.getAnnotation(Argument.class).optional(),
346-
fld.getType(),
347-
null,
348-
(name, val) -> {}
349-
));
323+
Consumer<Field> positionalArgCb = fld -> positionalArgs.add(argument(fld.getName().toLowerCase(), fld.getType())
324+
.withOptional(fld.getAnnotation(Argument.class).optional())
325+
.build()
326+
);
350327

351328
BiConsumer<ArgumentGroup, List<Field>> argGrpCb = (argGrp0, flds) -> flds.forEach(fld -> {
352329
if (fld.isAnnotationPresent(Positional.class))
@@ -359,6 +336,37 @@ private CLIArgumentParser createArgumentParser() {
359336

360337
namedArgs.addAll(common);
361338

362-
return new CLIArgumentParser(positionalArgs, namedArgs);
339+
return new CLIArgumentParser(positionalArgs, namedArgs, console);
340+
}
341+
342+
/** @return String representation of command with hidden values of sensitive arguments. */
343+
private String buildSafeCommandString(ListIterator<String> rawIter, CLIArgumentParser parser) {
344+
SB res = new SB();
345+
346+
while (rawIter.hasNext()) {
347+
String arg = rawIter.next();
348+
349+
CLIArgument<?> argDesc = parser.getArgumentDescriptor(arg.toLowerCase());
350+
351+
res.a(arg).a(' ');
352+
353+
if (argDesc == null || argDesc.isFlag())
354+
continue;
355+
356+
String argVal = readNextValueToken(rawIter);
357+
358+
if (argVal != null) {
359+
if (!argDesc.isSensitive())
360+
res.a(argVal).a(' ');
361+
else {
362+
res.a("***** ");
363+
364+
log.info(String.format("Warning: %s is insecure. Whenever possible, use interactive prompt for " +
365+
"password (just omit the argument value).", argDesc.name()));
366+
}
367+
}
368+
}
369+
370+
return res.toString();
363371
}
364372
}

modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/CommandHandler.java

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
import org.apache.ignite.internal.util.spring.IgniteSpringHelperImpl;
6161
import org.apache.ignite.internal.util.typedef.F;
6262
import org.apache.ignite.internal.util.typedef.X;
63-
import org.apache.ignite.internal.util.typedef.internal.SB;
6463
import org.apache.ignite.internal.util.typedef.internal.U;
6564
import org.apache.ignite.lang.IgniteExperimental;
6665
import org.apache.ignite.ssl.SslContextFactory;
@@ -247,7 +246,7 @@ public <A extends IgniteDataTransferObject> int execute(List<String> rawArgs) {
247246

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

250-
ConnectionAndSslParameters<A> args = new ArgumentParser(logger, registry).parseAndValidate(rawArgs);
249+
ConnectionAndSslParameters<A> args = new ArgumentParser(logger, registry, console).parseAndValidate(rawArgs);
251250

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

@@ -272,7 +271,7 @@ public <A extends IgniteDataTransferObject> int execute(List<String> rawArgs) {
272271
}
273272

274273
logger.info("Command [" + cmdName + "] started");
275-
logger.info("Arguments: " + argumentsToString(rawArgs));
274+
logger.info("Arguments: " + args.safeCommandString());
276275
logger.info(U.DELIM);
277276

278277
String deprecationMsg = args.command().deprecationMessage(args.commandArg());
@@ -445,36 +444,6 @@ private boolean isConnectionClosedSilentlyException(Throwable e) {
445444
return e instanceof ClientConnectionException && e.getMessage().startsWith("Channel is closed");
446445
}
447446

448-
/**
449-
* Joins user's arguments and hides sensitive information.
450-
*
451-
* @param rawArgs Arguments which user has provided.
452-
* @return String which could be shown in console and pritned to log.
453-
*/
454-
private String argumentsToString(List<String> rawArgs) {
455-
boolean hide = false;
456-
457-
SB sb = new SB();
458-
459-
for (int i = 0; i < rawArgs.size(); i++) {
460-
if (hide) {
461-
sb.a("***** ");
462-
463-
hide = false;
464-
465-
continue;
466-
}
467-
468-
String arg = rawArgs.get(i);
469-
470-
sb.a(arg).a(' ');
471-
472-
hide = ArgumentParser.isSensitiveArgument(arg);
473-
}
474-
475-
return sb.toString();
476-
}
477-
478447
/**
479448
* @param args Common arguments.
480449
* @return Thin client configuration to connect to cluster.
@@ -651,7 +620,8 @@ private void printHelp(List<String> rawArgs) {
651620
"The command has the following syntax:");
652621
logger.info("");
653622

654-
logger.info(INDENT + join(" ", join(" ", UTILITY_NAME, join(" ", new ArgumentParser(logger, registry).getCommonOptions())),
623+
logger.info(INDENT + join(" ",
624+
join(" ", UTILITY_NAME, join(" ", new ArgumentParser(logger, registry, null).getCommonOptions())),
655625
asOptional("command", true), "<command_parameters>"));
656626
logger.info("");
657627
logger.info("");
@@ -717,8 +687,8 @@ private void printCacheHelpHeader(IgniteLogger logger) {
717687
logger.info(INDENT + "The '--cache subcommand' is used to get information about and perform actions" +
718688
" with caches. The command has the following syntax:");
719689
logger.info("");
720-
logger.info(INDENT + join(" ", UTILITY_NAME, join(" ", new ArgumentParser(logger, null).getCommonOptions())) + " " +
721-
"--cache [subcommand] <subcommand_parameters>");
690+
logger.info(INDENT + join(" ", UTILITY_NAME, join(" ", new ArgumentParser(logger, null, null).getCommonOptions())) +
691+
" --cache [subcommand] <subcommand_parameters>");
722692
logger.info("");
723693
logger.info(INDENT + "The subcommands that take [nodeId] as an argument ('list', 'find_garbage', " +
724694
"'contention' and 'validate_indexes') will be executed on the given node or on all server nodes" +

modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/ConnectionAndSslParameters.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,25 @@ public class ConnectionAndSslParameters<A extends IgniteDataTransferObject> {
6565
/** */
6666
private final CLIArgumentParser parser;
6767

68+
/** String representation of command with hidden values of sensitive arguments. */
69+
private final String safeCmd;
70+
6871
/**
6972
* @param cmdPath Path to the command in {@link CommandsRegistry} hierarchy.
7073
* @param arg Command argument.
7174
* @param parser CLI arguments parser.
75+
* @param safeCmd String safe representation of command.
7276
*/
7377
public ConnectionAndSslParameters(
7478
Deque<Command<?, ?>> cmdPath,
7579
A arg,
76-
CLIArgumentParser parser
80+
CLIArgumentParser parser,
81+
String safeCmd
7782
) {
7883
this.cmdPath = cmdPath;
7984
this.arg = arg;
8085
this.parser = parser;
86+
this.safeCmd = safeCmd;
8187

8288
this.user = parser.get(CMD_USER);
8389
this.pwd = parser.get(CMD_PASSWORD);
@@ -249,4 +255,9 @@ public String sslFactoryConfigPath() {
249255
public boolean verbose() {
250256
return parser.get(CMD_VERBOSE);
251257
}
258+
259+
/** @return String representation of command with hidden values of sensitive arguments. */
260+
public String safeCommandString() {
261+
return safeCmd;
262+
}
252263
}

0 commit comments

Comments
 (0)