You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2017/09/12 18:36:52 UTC
[2/4] karaf git commit: [KARAF-5287] Provide a way to hide passwords
in shell
[KARAF-5287] Provide a way to hide passwords in shell
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/5228a23f
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/5228a23f
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/5228a23f
Branch: refs/heads/master
Commit: 5228a23fc4113922cc9936c8d921a97cc2ea434d
Parents: 1590a9a
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Mon Sep 11 09:10:45 2017 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Tue Sep 12 19:15:36 2017 +0200
----------------------------------------------------------------------
.../karaf/jaas/command/UserAddCommand.java | 2 +-
.../apache/karaf/shell/api/action/Argument.java | 15 ++
.../apache/karaf/shell/api/action/Option.java | 15 ++
.../action/command/ActionMaskingCallback.java | 161 +++++++++++++++++++
.../action/command/DefaultActionPreparator.java | 10 ++
.../shell/impl/action/command/HelpOption.java | 8 +
.../shell/impl/console/ConsoleSessionImpl.java | 60 ++++++-
.../command/ActionMaskingCallbackTest.java | 113 +++++++++++++
.../commands/AbstractCommandHelpPrinter.java | 8 +
9 files changed, 390 insertions(+), 2 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/jaas/command/src/main/java/org/apache/karaf/jaas/command/UserAddCommand.java
----------------------------------------------------------------------
diff --git a/jaas/command/src/main/java/org/apache/karaf/jaas/command/UserAddCommand.java b/jaas/command/src/main/java/org/apache/karaf/jaas/command/UserAddCommand.java
index 0a0898c..98b0546 100644
--- a/jaas/command/src/main/java/org/apache/karaf/jaas/command/UserAddCommand.java
+++ b/jaas/command/src/main/java/org/apache/karaf/jaas/command/UserAddCommand.java
@@ -27,7 +27,7 @@ public class UserAddCommand extends JaasCommandSupport {
@Argument(index = 0, name = "username", description = "User Name", required = true, multiValued = false)
private String username;
- @Argument(index = 1, name = "password", description = "Password", required = true, multiValued = false)
+ @Argument(index = 1, name = "password", description = "Password", required = true, multiValued = false, censor = true, mask = '#')
private String password;
@Override
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/main/java/org/apache/karaf/shell/api/action/Argument.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/action/Argument.java b/shell/core/src/main/java/org/apache/karaf/shell/api/action/Argument.java
index b0268a2..f9dcc1b 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/api/action/Argument.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/api/action/Argument.java
@@ -82,4 +82,19 @@ public @interface Argument
* @return the argument help string representation.
*/
String valueToShowInHelp() default DEFAULT_STRING;
+
+ /**
+ * Censor the argument in the console. Characters will be replaced with {@link Argument#mask()}.
+ * This is useful for hiding sensitive data like passwords.
+ *
+ * @return true if the argument should be censored in the console.
+ */
+ boolean censor() default false;
+
+ /**
+ * Character to use when censoring the argument in the console.
+ *
+ * @return the Character to use when censoring the argument in the console.
+ */
+ char mask() default '*';
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/main/java/org/apache/karaf/shell/api/action/Option.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/api/action/Option.java b/shell/core/src/main/java/org/apache/karaf/shell/api/action/Option.java
index bd15244..f4214ec 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/api/action/Option.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/api/action/Option.java
@@ -94,4 +94,19 @@ public @interface Option
* @return the option description as shown in the help.
*/
String valueToShowInHelp() default DEFAULT_STRING;
+
+ /**
+ * Censor the argument in the console. Characters will be replaced with {@link Argument#mask()}.
+ * This is useful for hiding sensitive data like passwords.
+ *
+ * @return true if the argument should be censored in the console.
+ */
+ boolean censor() default false;
+
+ /**
+ * Character to use when censoring the argument in the console.
+ *
+ * @return the Character to use when censoring the argument in the console.
+ */
+ char mask() default '*';
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallback.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallback.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallback.java
new file mode 100644
index 0000000..33c7dbe
--- /dev/null
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallback.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.karaf.shell.impl.action.command;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Option;
+import org.jline.reader.ParsedLine;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ActionMaskingCallback {
+
+ private final ActionCommand command;
+ private final Set<String> booleanOptions;
+ private final Map<String, Option> typedOptions;
+ private final List<Argument> arguments;
+
+ public static ActionMaskingCallback build(ActionCommand command) {
+ Set<String> booleanOptions = new HashSet<>();
+ Map<String, Option> typedOptions = new HashMap<>();
+ List<Argument> arguments = new ArrayList<>();
+ boolean censor = false;
+ for (Class<?> type = command.getActionClass(); type != null; type = type.getSuperclass()) {
+ for (Field field : type.getDeclaredFields()) {
+ Option option = field.getAnnotation(Option.class);
+ if (option != null) {
+ if (field.getType() == boolean.class || field.getType() == Boolean.class) {
+ booleanOptions.add(option.name());
+ booleanOptions.addAll(Arrays.asList(option.aliases()));
+ } else {
+ typedOptions.put(option.name(), option);
+ Arrays.asList(option.aliases()).forEach(action -> typedOptions.put(option.name(), option));
+ censor |= option.censor();
+ }
+ }
+ Argument argument = field.getAnnotation(Argument.class);
+ if (argument != null) {
+ arguments.add(argument);
+ censor |= argument.censor();
+ }
+ }
+ }
+ arguments.sort(Comparator.comparing(Argument::index));
+ return censor ? new ActionMaskingCallback(command, booleanOptions, typedOptions, arguments) : null;
+ }
+
+ private ActionMaskingCallback(ActionCommand command, Set<String> booleanOptions, Map<String, Option> typedOptions, List<Argument> arguments) {
+ this.command = command;
+ this.booleanOptions = booleanOptions;
+ this.typedOptions = typedOptions;
+ this.arguments = arguments;
+ }
+
+ public String filter(String line, ParsedLine parsed) {
+ int prev = 0;
+ StringBuilder sb = new StringBuilder();
+ int cur = line.indexOf(parsed.line());
+ List<String> words = parsed.words();
+ int state = 0;
+ int arg = 0;
+ for (int word = 0; word < words.size(); word++) {
+ String wordStr = words.get(word);
+ switch (state) {
+ // command
+ case 0:
+ cur = line.indexOf(wordStr, cur) + wordStr.length();
+ state++;
+ break;
+ // option
+ case 1:
+ if (wordStr.startsWith("-")) {
+ int idxEq = wordStr.indexOf('=');
+ if (idxEq > 0) {
+ String name = wordStr.substring(0, idxEq);
+ if (booleanOptions.contains(name)) {
+ break;
+ }
+ Option option = typedOptions.get(name);
+ if (option != null && option.censor()) {
+ cur = line.indexOf(wordStr, cur);
+ sb.append(line.substring(prev, cur + idxEq + 1));
+ for (int i = idxEq + 1; i < wordStr.length(); i++) {
+ sb.append(option.mask());
+ }
+ prev = cur = cur + wordStr.length();
+ }
+ } else {
+ String name = wordStr;
+ if (booleanOptions.contains(name)) {
+ break;
+ }
+ Option option = typedOptions.get(name);
+ if (option != null) {
+ // skip value
+ word++;
+ if (option.censor() && word < words.size()) {
+ String val = words.get(word);
+ cur = line.indexOf(val, cur);
+ sb.append(line.substring(prev, cur));
+ for (int i = 0; i < val.length(); i++) {
+ sb.append(option.mask());
+ }
+ prev = cur = cur + val.length();
+ }
+ }
+ }
+ break;
+ } else {
+ state = 2;
+ // fall through
+ }
+ // argument
+ case 2:
+ if (arg < arguments.size()) {
+ Argument argument = arguments.get(arg);
+ if (argument.censor()) {
+ cur = line.indexOf(wordStr, cur);
+ sb.append(line.substring(prev, cur));
+ for (int i = 0; i < wordStr.length(); i++) {
+ sb.append(argument.mask());
+ }
+ prev = cur = cur + wordStr.length();
+ }
+ if (!argument.multiValued()) {
+ arg++;
+ }
+ }
+ break;
+ }
+ }
+ if (prev < line.length()) {
+ sb.append(line, prev, line.length());
+ }
+ return sb.toString();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
index 313e2a2..b4ceb27 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/DefaultActionPreparator.java
@@ -283,6 +283,16 @@ public class DefaultActionPreparator {
public Class<? extends Annotation> annotationType() {
return delegate.annotationType();
}
+
+ @Override
+ public boolean censor() {
+ return delegate.censor();
+ }
+
+ @Override
+ public char mask() {
+ return delegate.mask();
+ }
};
}
return argument;
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
index ec0b1a8..8a409a0 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/action/command/HelpOption.java
@@ -50,6 +50,14 @@ public class HelpOption {
return Option.DEFAULT_STRING;
}
+ public boolean censor() {
+ return false;
+ }
+
+ public char mask() {
+ return 0;
+ }
+
public Class<? extends Annotation> annotationType() {
return Option.class;
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
----------------------------------------------------------------------
diff --git a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
index 7fd3ced..34795ed 100644
--- a/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
+++ b/shell/core/src/main/java/org/apache/karaf/shell/impl/console/ConsoleSessionImpl.java
@@ -27,6 +27,8 @@ import java.lang.management.ManagementFactory;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -53,6 +55,8 @@ import org.apache.karaf.shell.api.console.Registry;
import org.apache.karaf.shell.api.console.Session;
import org.apache.karaf.shell.api.console.SessionFactory;
import org.apache.karaf.shell.api.console.Terminal;
+import org.apache.karaf.shell.impl.action.command.ActionCommand;
+import org.apache.karaf.shell.impl.action.command.ActionMaskingCallback;
import org.apache.karaf.shell.impl.console.parsing.CommandLineParser;
import org.apache.karaf.shell.impl.console.parsing.KarafParser;
import org.apache.karaf.shell.support.ShellUtil;
@@ -105,6 +109,7 @@ public class ConsoleSessionImpl implements Session {
final org.jline.terminal.Terminal jlineTerminal;
final History history;
final LineReader reader;
+ final AggregateMaskingCallback maskingCallback;
private Thread thread;
private Properties brandingProps;
@@ -159,6 +164,9 @@ public class ConsoleSessionImpl implements Session {
commandsCompleter.complete(rdr, line, candidates);
};
+ // Masking
+ maskingCallback = new AggregateMaskingCallback();
+
// Console reader
reader = LineReaderBuilder.builder()
.terminal(jlineTerminal)
@@ -411,7 +419,7 @@ public class ConsoleSessionImpl implements Session {
CharSequence command = null;
reading.set(true);
try {
- reader.readLine(getPrompt(), getRPrompt(), (MaskingCallback) null, null);
+ reader.readLine(getPrompt(), getRPrompt(), maskingCallback, null);
ParsedLine pl = reader.getParsedLine();
if (pl instanceof ParsedLineImpl) {
command = ((ParsedLineImpl) pl).program();
@@ -616,4 +624,54 @@ public class ConsoleSessionImpl implements Session {
return parts[0];
}
+ private class AggregateMaskingCallback implements MaskingCallback {
+
+ private final List<Command> commands = new ArrayList<>();
+ private final Map<String, ActionMaskingCallback> regexs = new HashMap<>();
+
+ @Override
+ public String display(String line) {
+ return compute(line);
+ }
+
+ @Override
+ public String history(String line) {
+ return compute(line);
+ }
+
+ private String compute(String line) {
+ Collection<Command> commands;
+ boolean update;
+ synchronized (this) {
+ commands = factory.getRegistry().getCommands();
+ update = !commands.equals(this.commands);
+ }
+ if (update) {
+ Map<String, ActionMaskingCallback> regexs = new HashMap<>();
+ for (Command cmd : commands) {
+ if (cmd instanceof ActionCommand) {
+ ActionMaskingCallback amc = ActionMaskingCallback.build((ActionCommand) cmd);
+ if (amc != null) {
+ regexs.put(cmd.getScope() + ":" + cmd.getName(), amc);
+ }
+ }
+ }
+ synchronized (this) {
+ this.commands.clear();
+ this.regexs.clear();
+ this.commands.addAll(commands);
+ this.regexs.putAll(regexs);
+ }
+ }
+ ParsedLine pl = reader.getParser().parse(line, line.length());
+ String cmd = resolveCommand(pl.words().get(0));
+ ActionMaskingCallback repl = regexs.get(cmd);
+ if (repl != null) {
+ line = repl.filter(line, pl);
+ }
+ return line;
+ }
+
+ }
+
}
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallbackTest.java
----------------------------------------------------------------------
diff --git a/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallbackTest.java b/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallbackTest.java
new file mode 100644
index 0000000..672afdd
--- /dev/null
+++ b/shell/core/src/test/java/org/apache/karaf/shell/impl/action/command/ActionMaskingCallbackTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.karaf.shell.impl.action.command;
+
+import org.apache.felix.gogo.runtime.CommandProcessorImpl;
+import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.api.console.SessionFactory;
+import org.apache.karaf.shell.impl.console.HeadlessSessionImpl;
+import org.apache.karaf.shell.impl.console.SessionFactoryImpl;
+import org.apache.karaf.shell.impl.console.parsing.KarafParser;
+import org.jline.reader.ParsedLine;
+import org.jline.reader.Parser;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.PrintStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ActionMaskingCallbackTest {
+
+ private Parser parser;
+ private ActionMaskingCallback cb;
+
+ @Before
+ public void setUp() {
+ ThreadIO tio = new ThreadIOImpl();
+ CommandProcessor cp = new CommandProcessorImpl(tio);
+ SessionFactory sf = new SessionFactoryImpl(tio);
+ InputStream is = new ByteArrayInputStream(new byte[0]);
+ PrintStream os = new PrintStream(new ByteArrayOutputStream());
+ Session session = new HeadlessSessionImpl(sf, cp, is, os, os);
+ parser = new KarafParser(session);
+
+ ActionCommand cmd = new ActionCommand(null, UserAddCommand.class);
+ cb = ActionMaskingCallback.build(cmd);
+ }
+
+ @Test
+ public void testJaasUserAdd() throws Exception {
+ check("user-add user password ", "user-add user ######## ");
+ check("user-add --help user password ", "user-add --help user ######## ");
+ check("user-add --help user password foo", "user-add --help user ######## foo");
+ check("user-add --opt1 user password foo", "user-add --opt1 user ######## foo");
+ check("user-add --opt2 valOpt2 user password foo", "user-add --opt2 valOpt2 user ######## foo");
+ check("user-add --opt2=valOpt2 user password foo", "user-add --opt2=valOpt2 user ######## foo");
+ check("user-add --opt1 --opt2 valOpt2 --opt3=valOpt3 user password foo", "user-add --opt1 --opt2 valOpt2 --opt3=@@@@@@@ user ######## foo");
+ check("user-add --opt1 --opt2 valOpt2 --opt3 valOpt3 user password foo", "user-add --opt1 --opt2 valOpt2 --opt3 @@@@@@@ user ######## foo");
+ }
+
+ private void check(String input, String expected) {
+ String output = cb.filter(input, parser.parse(input, input.length()));
+ assertEquals(expected, output);
+ }
+
+
+ @Command(scope = "jaas", name = "user-add", description = "Add a user")
+ @Service
+ public static class UserAddCommand implements Action {
+
+ @Option(name = "--opt1")
+ private boolean opt1;
+
+ @Option(name = "--opt2")
+ private String opt2;
+
+ @Option(name = "--opt3", censor = true, mask = '@')
+ private String opt3;
+
+ @Argument(index = 0, name = "username")
+ private String username;
+
+ @Argument(index = 1, name = "password", censor = true, mask = '#')
+ private String password;
+
+ @Argument(index = 2, name = "foo")
+ private String foo;
+
+ @Override
+ public Object execute() throws Exception {
+ return null;
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/5228a23f/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/commands/AbstractCommandHelpPrinter.java
----------------------------------------------------------------------
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/commands/AbstractCommandHelpPrinter.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/commands/AbstractCommandHelpPrinter.java
index dee1eb9..eecad47 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/commands/AbstractCommandHelpPrinter.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/commands/AbstractCommandHelpPrinter.java
@@ -58,6 +58,14 @@ public abstract class AbstractCommandHelpPrinter implements CommandHelpPrinter {
public Class<? extends Annotation> annotationType() {
return delegate.annotationType();
}
+
+ public boolean censor() {
+ return delegate.censor();
+ }
+
+ public char mask() {
+ return delegate.mask();
+ }
};
}
return argument;