diff --git a/pom.xml b/pom.xml index 87d9212a0f9..cd54afa434c 100644 --- a/pom.xml +++ b/pom.xml @@ -301,7 +301,7 @@ jsr166e/** - org/elasticsearch/shield/support/CmdLineTool* + org/elasticsearch/shield/support/cli/Terminal* org/elasticsearch/common/logging/log4j/ConsoleAppender* org/elasticsearch/plugins/PluginManager.class org/elasticsearch/bootstrap/Bootstrap.class diff --git a/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStore.java b/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStore.java index a1ccfdccc5b..2cf57f52769 100644 --- a/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStore.java +++ b/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStore.java @@ -51,7 +51,7 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd FileUserPasswdStore(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) { super(settings); - file = resolveFile(componentSettings, env); + file = resolveFile(settings, env); esUsers = ImmutableMap.copyOf(parseFile(file, logger)); watcher = new FileWatcher(file.getParent().toFile()); watcher.addListener(new FileListener()); @@ -72,7 +72,7 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd } public static Path resolveFile(Settings settings, Environment env) { - String location = settings.get("file.users"); + String location = settings.get("shield.authc.esusers.files.users"); if (location == null) { return env.configFile().toPath().resolve(".users"); } @@ -110,9 +110,9 @@ public class FileUserPasswdStore extends AbstractComponent implements UserPasswd } public static void writeFile(Map esUsers, Path path) { - try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path, Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE))) { + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path, Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE))) { for (Map.Entry entry : esUsers.entrySet()) { - writer.printf(Locale.ROOT, "{}\t{}", entry.getKey(), new String(entry.getValue())); + writer.printf(Locale.ROOT, "%s:%s%s", entry.getKey(), new String(entry.getValue()), System.lineSeparator()); } } catch (IOException ioe) { throw new ElasticsearchException("Could not write users file [" + path.toAbsolutePath() + "], please check file permissions", ioe); diff --git a/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStore.java b/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStore.java index 20df4811896..231b3cc2ffb 100644 --- a/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStore.java +++ b/src/main/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStore.java @@ -8,6 +8,7 @@ package org.elasticsearch.shield.authc.esusers; import com.google.common.base.Charsets; import com.google.common.collect.ImmutableMap; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.collect.Lists; import org.elasticsearch.common.inject.internal.Nullable; import org.elasticsearch.common.Strings; import org.elasticsearch.common.component.AbstractComponent; @@ -52,7 +53,7 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt FileUserRolesStore(Settings settings, Environment env, ResourceWatcherService watcherService, Listener listener) { super(settings); - file = resolveFile(componentSettings, env); + file = resolveFile(settings, env); userRoles = ImmutableMap.copyOf(parseFile(file, logger)); FileWatcher watcher = new FileWatcher(file.getParent().toFile()); watcher.addListener(new FileListener()); @@ -65,7 +66,7 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt } public static Path resolveFile(Settings settings, Environment env) { - String location = settings.get("file.users_roles"); + String location = settings.get("shield.authc.esusers.files.users_roles"); if (location == null) { return env.configFile().toPath().resolve(".users_roles"); } @@ -104,9 +105,9 @@ public class FileUserRolesStore extends AbstractComponent implements UserRolesSt } public static void writeFile(Map userRoles, Path path) { - try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path, Charsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.WRITE))) { + try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(path, Charsets.UTF_8, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE))) { for (Map.Entry entry : userRoles.entrySet()) { - writer.printf(Locale.ROOT, "{}\t{}", entry.getKey(), Strings.arrayToCommaDelimitedString(entry.getValue())); + writer.printf(Locale.ROOT, "%s:%s%s", entry.getKey(), Strings.arrayToCommaDelimitedString(entry.getValue()), System.lineSeparator()); } } catch (IOException ioe) { throw new ElasticsearchException("Could not write users file [" + path.toAbsolutePath() + "], please check file permissions"); diff --git a/src/main/java/org/elasticsearch/shield/authc/esusers/tool/ESUsersTool.java b/src/main/java/org/elasticsearch/shield/authc/esusers/tool/ESUsersTool.java new file mode 100644 index 00000000000..e90d2b42eb0 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/authc/esusers/tool/ESUsersTool.java @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.authc.esusers.tool; + +import org.apache.commons.cli.CommandLine; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.shield.authc.esusers.FileUserPasswdStore; +import org.elasticsearch.shield.authc.esusers.FileUserRolesStore; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.support.cli.CliTool; +import org.elasticsearch.shield.support.cli.CliToolConfig; +import org.elasticsearch.shield.support.cli.Terminal; + +import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.elasticsearch.shield.support.cli.CliToolConfig.Builder.cmd; +import static org.elasticsearch.shield.support.cli.CliToolConfig.Builder.option; + +/** + * + */ +public class ESUsersTool extends CliTool { + + private static final CliToolConfig CONFIG = CliToolConfig.config("esusers", ESUsersTool.class) + .cmds(Useradd.CMD, Userdel.CMD, Passwd.CMD) + .build(); + + public ESUsersTool() { + super(CONFIG); + } + + public ESUsersTool(Terminal terminal) { + super(CONFIG, terminal); + } + + @Override + protected Command parse(String cmdName, CommandLine cli) throws Exception { + switch (cmdName.toLowerCase(Locale.ROOT)) { + case Useradd.NAME: return Useradd.parse(terminal, cli); + case Userdel.NAME: return Userdel.parse(terminal, cli); + case Passwd.NAME: return Passwd.parse(terminal, cli); + default: + assert false : "should never get here, if the user enters an unknown command, an error message should be shown before parse is called"; + return null; + } + } + + static class Useradd extends CliTool.Command { + + private static final String NAME = "useradd"; + + private static final CliToolConfig.Cmd CMD = cmd(NAME, Useradd.class) + .options( + option("p", "password").hasArg(false).required(false), + option("r", "roles").hasArg(false).required(false)) + .build(); + + public static Command parse(Terminal terminal, CommandLine cli) { + if (cli.getArgs().length == 0) { + return exitCmd(ExitStatus.USAGE, terminal, "username is missing"); + } + + String username = cli.getArgs()[0]; + + char[] password; + String passwordStr = cli.getOptionValue("password"); + if (passwordStr != null) { + password = passwordStr.toCharArray(); + } else { + password = terminal.readSecret("Enter new password: "); + char[] retyped = terminal.readSecret("Retype new password: "); + if (!Arrays.equals(password, retyped)) { + return exitCmd(ExitStatus.USAGE, terminal, "Password mismatch"); + } + } + + String rolesCsv = cli.getOptionValue("roles"); + String[] roles = (rolesCsv != null) ? rolesCsv.split(",") : Strings.EMPTY_ARRAY; + return new Useradd(terminal, username, password, roles); + } + + final String username; + final char[] passwd; + final String[] roles; + + Useradd(Terminal terminal, String username, char[] passwd, String... roles) { + super(terminal); + this.username = username; + this.passwd = passwd; + this.roles = roles; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + Path file = FileUserPasswdStore.resolveFile(settings, env); + Map users = new HashMap<>(FileUserPasswdStore.parseFile(file, null)); + if (users == null) { + // file doesn't exist so we just create a new file + users = new HashMap<>(); + } + if (users.containsKey(username)) { + terminal.println("User [{}] already exists", username); + return ExitStatus.CODE_ERROR; + } + Hasher hasher = Hasher.HTPASSWD; + users.put(username, hasher.hash(passwd)); + FileUserPasswdStore.writeFile(users, file); + + + file = FileUserRolesStore.resolveFile(settings, env); + Map userRoles = new HashMap<>(FileUserRolesStore.parseFile(file, null)); + if (userRoles == null) { + // file doesn't exist, so we just create a new file + userRoles = new HashMap<>(); + } + userRoles.put(username, roles); + FileUserRolesStore.writeFile(userRoles, file); + return ExitStatus.OK; + } + } + + static class Userdel extends CliTool.Command { + + private static final String NAME = "userdel"; + + private static final CliToolConfig.Cmd CMD = cmd(NAME, Userdel.class).build(); + + public static Command parse(Terminal terminal, CommandLine cli) { + if (cli.getArgs().length == 0) { + return exitCmd(ExitStatus.USAGE, terminal, "username is missing"); + } + + String username = cli.getArgs()[0]; + return new Userdel(terminal, username); + } + + final String username; + + Userdel(Terminal terminal, String username) { + super(terminal); + this.username = username; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + Path file = FileUserPasswdStore.resolveFile(settings, env); + Map users = new HashMap<>(FileUserPasswdStore.parseFile(file, null)); + if (users != null) { + char[] passwd = users.remove(username); + if (passwd != null) { + FileUserPasswdStore.writeFile(users, file); + } else { + terminal.println("Warning: users file [%s] did not contain password entry for user [%s]", file.toAbsolutePath(), username); + } + } + + file = FileUserRolesStore.resolveFile(settings, env); + Map userRoles = new HashMap<>(FileUserRolesStore.parseFile(file, null)); + if (userRoles != null) { + String[] roles = userRoles.remove(username); + if (roles != null) { + FileUserRolesStore.writeFile(userRoles, file); + } else { + terminal.println("Warning: users_roles file [%s] did not contain roles entry for user [%s]", file.toAbsolutePath(), username); + } + } + + return ExitStatus.OK; + } + } + + static class Passwd extends CliTool.Command { + + private static final String NAME = "passwd"; + + private static final CliToolConfig.Cmd CMD = cmd(NAME, Passwd.class) + .options(option("p", "password").hasArg(false).required(false)) + .build(); + + public static Command parse(Terminal terminal, CommandLine cli) { + if (cli.getArgs().length == 0) { + return exitCmd(ExitStatus.USAGE, terminal, "username is missing"); + } + + String username = cli.getArgs()[0]; + + char[] password; + String passwordStr = cli.getOptionValue("password"); + if (passwordStr != null) { + password = passwordStr.toCharArray(); + } else { + password = terminal.readSecret("Enter new password: "); + char[] retyped = terminal.readSecret("Retype new password: "); + if (!Arrays.equals(password, retyped)) { + return exitCmd(ExitStatus.USAGE, terminal, "Password mismatch"); + } + } + return new Passwd(terminal, username, password); + } + + final String username; + final char[] passwd; + + Passwd(Terminal terminal, String username, char[] passwd) { + super(terminal); + this.username = username; + this.passwd = passwd; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + Path file = FileUserPasswdStore.resolveFile(settings, env); + Map users = new HashMap<>(FileUserPasswdStore.parseFile(file, null)); + if (users == null) { + // file doesn't exist so we just create a new file + users = new HashMap<>(); + } + if (!users.containsKey(username)) { + terminal.println("User [{}] doesn't exist", username); + return ExitStatus.NO_USER; + } + Hasher hasher = Hasher.HTPASSWD; + users.put(username, hasher.hash(passwd)); + FileUserPasswdStore.writeFile(users, file); + return ExitStatus.OK; + } + } +} diff --git a/src/main/java/org/elasticsearch/shield/authc/esusers/tool/UserAdd.java b/src/main/java/org/elasticsearch/shield/authc/esusers/tool/UserAdd.java deleted file mode 100644 index 015b28d503b..00000000000 --- a/src/main/java/org/elasticsearch/shield/authc/esusers/tool/UserAdd.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.shield.authc.esusers.tool; - -import org.apache.commons.cli.CommandLine; -import org.elasticsearch.shield.authc.esusers.FileUserPasswdStore; -import org.elasticsearch.shield.authc.esusers.FileUserRolesStore; -import org.elasticsearch.shield.authc.support.Hasher; -import org.elasticsearch.shield.support.CmdLineTool; - -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * - */ -public class UserAdd extends CmdLineTool { - - public static void main(String[] args) throws Exception { - new UserAdd().execute(args); - } - - public UserAdd() { - super("useradd", - option("p", "password", "The user password").hasArg(true).required(true), - option("r", "roles", "Comma-separated list of the roles of the user").hasArg(true).required(true), - option("h", "help", "Prints usage help").hasArg(false).required(false) - ); - } - - public void run(CommandLine cli) throws Exception { - - if (cli.getArgs().length == 0) { - terminal.println("username is missing"); - printUsage(); - exit(ExitStatus.USAGE); - } - - String username = cli.getArgs()[0]; - - - char[] password; - String passwordStr = cli.getOptionValue("password"); - if (passwordStr != null) { - password = passwordStr.toCharArray(); - } else { - password = terminal.readPassword("Enter new password: "); - char[] retyped = terminal.readPassword("Retype new password: "); - if (!Arrays.equals(password, retyped)) { - terminal.print("Password mismatch"); - exit(ExitStatus.USAGE); - } - } - - String[] roles = null; - String rolesCsv = cli.getOptionValue("roles"); - if (rolesCsv != null) { - roles = rolesCsv.split(","); - } - addUser(username, password, roles); - } - - private void addUser(String username, char[] passwd, String[] roles) { - Path file = FileUserPasswdStore.resolveFile(settings, env); - Map users = FileUserPasswdStore.parseFile(file, null); - if (users == null) { - // file doesn't exist so we just create a new file - users = new HashMap<>(); - } - if (users.containsKey(username)) { - terminal.println("User [{}] already exists", username); - exit(ExitStatus.CODE_ERROR); - } - Hasher hasher = Hasher.HTPASSWD; - users.put(username, hasher.hash(passwd)); - FileUserPasswdStore.writeFile(users, file); - - file = FileUserRolesStore.resolveFile(settings, env); - Map userRoles = FileUserRolesStore.parseFile(file, null); - if (userRoles == null) { - // file doesn't exist, so we just create a new file - userRoles = new HashMap<>(); - } - userRoles.put(username, roles); - FileUserRolesStore.writeFile(userRoles, file); - } - -} diff --git a/src/main/java/org/elasticsearch/shield/support/CmdLineTool.java b/src/main/java/org/elasticsearch/shield/support/CmdLineTool.java deleted file mode 100644 index e220dc8f435..00000000000 --- a/src/main/java/org/elasticsearch/shield/support/CmdLineTool.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -package org.elasticsearch.shield.support; - -import org.apache.commons.cli.*; -import org.elasticsearch.common.collect.Tuple; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.env.Environment; -import org.elasticsearch.node.internal.InternalSettingsPreparer; - -import java.io.*; -import java.util.Locale; - -import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS; - -/** - * - */ -public abstract class CmdLineTool { - - protected enum ExitStatus { - OK(0), - USAGE(64), - IO_ERROR(74), - CODE_ERROR(70); - - private final int status; - - private ExitStatus(int status) { - this.status = status; - } - } - - protected static final Terminal terminal = ConsoleTerminal.supported() ? new ConsoleTerminal() : new SystemTerminal(); - protected static final Environment env; - protected static final Settings settings; - - static { - Tuple tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true); - settings = tuple.v1(); - env = tuple.v2(); - } - - private final Options options; - private final String cmd; - private final HelpFormatter helpFormatter = new HelpFormatter(); - - protected CmdLineTool(String cmd, OptionBuilder... options) { - this.cmd = cmd; - this.options = new Options(); - for (int i = 0; i < options.length; i++) { - this.options.addOption(options[i].option); - } - } - - protected final void execute(String[] args) { - CommandLineParser parser = new GnuParser(); - try { - CommandLine cli = parser.parse(options, args); - run(cli); - } catch (ParseException pe) { - printUsage(); - } catch (Exception e) { - terminal.println("Error: %s", e.getMessage()); - printUsage(); - } - - } - - protected void printUsage() { - helpFormatter.printUsage(terminal.printWriter(), HelpFormatter.DEFAULT_WIDTH, cmd, options); - } - - protected void exit(ExitStatus status) { - System.exit(status.status); - } - - protected abstract void run(CommandLine cli) throws Exception; - - protected static OptionBuilder option(String shortName, String longName, String description) { - return new OptionBuilder(shortName, longName, description); - } - - protected static class OptionBuilder { - - private final Option option; - - private OptionBuilder(String shortName, String longName, String description) { - option = new Option(shortName, description); - option.setLongOpt(longName); - } - - public OptionBuilder required(boolean required) { - option.setRequired(required); - return this; - } - - public OptionBuilder hasArg(boolean hasArg) { - if (hasArg) { - option.setArgs(1); - } - return this; - } - - } - - protected static abstract class Terminal { - - public abstract void print(String msg, Object... args); - - public void println(String msg, Object... args) { - print(msg + System.lineSeparator(), args); - } - - public abstract void print(Throwable t); - - public void newLine() { - println(""); - } - - public abstract String readString(String msg, Object... args); - - public abstract char[] readPassword(String msg, Object... args); - - public abstract PrintWriter printWriter(); - } - - private static class ConsoleTerminal extends Terminal { - - final Console console = System.console(); - - static boolean supported() { - return System.console() != null; - } - - @Override - public void print(String msg, Object... args) { - console.printf(msg, args); - console.flush(); - } - - @Override - public void print(Throwable t) { - t.printStackTrace(console.writer()); - console.flush(); - } - - @Override - public String readString(String msg, Object... args) { - return console.readLine(msg, args); - } - - @Override - public char[] readPassword(String msg, Object... args) { - return console.readPassword(msg, args); - } - - @Override - public PrintWriter printWriter() { - return console.writer(); - } - } - - private static class SystemTerminal extends Terminal { - - private final PrintWriter printWriter = new PrintWriter(System.out); - - @Override - public void print(String msg, Object... args) { - System.out.print(String.format(Locale.ROOT, msg, args)); - } - - @Override - public void print(Throwable t) { - t.printStackTrace(System.err); - } - - @Override - public String readString(String msg, Object... args) { - print(msg, args); - BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); - try { - return reader.readLine(); - } catch (IOException ioe) { - System.err.println("Could not read input"); - ioe.printStackTrace(); - System.exit(1); - } - return null; - } - - @Override - public char[] readPassword(String msg, Object... args) { - return readString(msg, args).toCharArray(); - } - - @Override - public PrintWriter printWriter() { - return printWriter; - } - } - -} - diff --git a/src/main/java/org/elasticsearch/shield/support/cli/CliTool.java b/src/main/java/org/elasticsearch/shield/support/cli/CliTool.java new file mode 100644 index 00000000000..85b521e4aa9 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/support/cli/CliTool.java @@ -0,0 +1,222 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.support.cli; + +import com.google.common.base.Preconditions; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.GnuParser; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.node.internal.InternalSettingsPreparer; + +import java.io.IOException; +import java.util.Locale; + +import static org.elasticsearch.common.settings.ImmutableSettings.Builder.EMPTY_SETTINGS; + +/** + * A base class for command-line interface tool. + * + * Two modes are supported: + * + * - Singe command mode. The tool exposes a single command that can potentially accept arguments (eg. CLI options). + * - Multi command mode. The tool support multiple command, each for different tasks, each potentially accepts arguments. + * + * In a multi-command mode. The first argument must be the command name. For example, the plugin manager + * can be seen as a multi-command tool with two possible commands: install and uninstall + * + * The tool is configured using a {@link CliToolConfig} which encapsulates the tool's commands and their + * potential options. The tool also comes with out of the box simple help support (the -h/--help option is + * automatically handled) where the help text is configured in a dedicated *.help files located in the same package + * as the tool. + */ +public abstract class CliTool { + + // based on sysexits.h + public static enum ExitStatus { + OK(0), + USAGE(64), /* command line usage error */ + DATA_ERROR(65), /* data format error */ + NO_INPUT(66), /* cannot open input */ + NO_USER(67), /* addressee unknown */ + NO_HOST(68), /* host name unknown */ + UNAVAILABLE(69), /* service unavailable */ + CODE_ERROR(70), /* internal software error */ + CANT_CREATE(73), /* can't create (user) output file */ + IO_ERROR(74), /* input/output error */ + TEMP_FAILURE(75), /* temp failure; user is invited to retry */ + PROTOCOL(76), /* remote error in protocol */ + NOPERM(77), /* permission denied */ + CONFIG(78); /* configuration error */ + + final int status; + + private ExitStatus(int status) { + this.status = status; + } + + public int status() { + return status; + } + } + + protected final Terminal terminal; + protected final Environment env; + protected final Settings settings; + + private final CliToolConfig config; + + protected CliTool(CliToolConfig config) { + this(config, Terminal.INSTANCE); + } + + protected CliTool(CliToolConfig config, Terminal terminal) { + Preconditions.checkArgument(config.cmds().size() != 0, "At least one command must be configured"); + this.config = config; + this.terminal = terminal; + Tuple tuple = InternalSettingsPreparer.prepareSettings(EMPTY_SETTINGS, true); + settings = tuple.v1(); + env = tuple.v2(); + } + + public final int execute(String... args) { + + // first lets see if the user requests tool help. We're doing it only if + // this is a multi-command tool. If it's a single command tool, the -h/--help + // option will be taken care of on the command level + if (!config.isSingle() && args.length > 0 && (args[0].equals("-h") || args[0].equals("--help"))) { + config.printUsage(terminal); + return ExitStatus.OK.status; + } + + CliToolConfig.Cmd cmd = null; + if (config.isSingle()) { + cmd = config.single(); + } else { + + if (args.length == 0) { + terminal.println("Error: command not specified"); + config.printUsage(terminal); + return ExitStatus.USAGE.status; + } + + String cmdName = args[0]; + cmd = config.cmd(cmdName); + if (cmd == null) { + terminal.println("Error: unknown command [%s]. Use [-h] option to list available commands", cmdName); + return ExitStatus.USAGE.status; + } + + // we now remove the command name from the args + if (args.length == 1) { + args = new String[0]; + } else { + String[] cmdArgs = new String[args.length - 1]; + System.arraycopy(args, 1, cmdArgs, 0, cmdArgs.length); + args = cmdArgs; + } + } + + Command command = null; + try { + command = parse(cmd, args); + return command.execute(settings, env).status; + } catch (IOException ioe) { + terminal.println(ioe.getMessage()); + return ExitStatus.IO_ERROR.status; + } catch (Exception e) { + terminal.println(e.getMessage()); + if (command == null) { + return ExitStatus.USAGE.status; + } + return ExitStatus.CODE_ERROR.status; + } + } + + public Command parse(String cmdName, String[] args) throws Exception { + CliToolConfig.Cmd cmd = config.cmd(cmdName); + return parse(cmd, args); + } + + public Command parse(CliToolConfig.Cmd cmd, String[] args) throws Exception { + CommandLineParser parser = new GnuParser(); + CommandLine cli = parser.parse(cmd.options(), args); + if (cli.hasOption("h")) { + return helpCmd(cmd); + } + return parse(cmd.name(), cli); + } + + protected Command.Help helpCmd(CliToolConfig.Cmd cmd) { + return new Command.Help(cmd, terminal); + } + + protected static Command.Exit exitCmd(ExitStatus status) { + return new Command.Exit(null, status, null); + } + + protected static Command.Exit exitCmd(ExitStatus status, Terminal terminal, String msg, Object... args) { + return new Command.Exit(String.format(Locale.ROOT, msg, args), status, terminal); + } + + protected abstract Command parse(String cmdName, CommandLine cli) throws Exception; + + public static abstract class Command { + + protected final Terminal terminal; + + protected Command(Terminal terminal) { + this.terminal = terminal; + } + + public abstract ExitStatus execute(Settings settings, Environment env) throws Exception; + + public static class Help extends Command { + + private final CliToolConfig.Cmd cmd; + + private Help(CliToolConfig.Cmd cmd, Terminal terminal) { + super(terminal); + this.cmd = cmd; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + cmd.printUsage(terminal); + return ExitStatus.OK; + } + } + + public static class Exit extends Command { + private final String msg; + private final ExitStatus status; + + private Exit(String msg, ExitStatus status, Terminal terminal) { + super(terminal); + this.msg = msg; + this.status = status; + } + + @Override + public ExitStatus execute(Settings settings, Environment env) throws Exception { + if (msg != null) { + terminal.println(msg); + } + return status; + } + + public ExitStatus status() { + return status; + } + } + } + + + +} + diff --git a/src/main/java/org/elasticsearch/shield/support/cli/CliToolConfig.java b/src/main/java/org/elasticsearch/shield/support/cli/CliToolConfig.java new file mode 100644 index 00000000000..94604b9819e --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/support/cli/CliToolConfig.java @@ -0,0 +1,183 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.support.cli; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; + +import java.util.Collection; + +/** +* +*/ +public class CliToolConfig { + + public static Builder config(String name, Class toolType) { + return new Builder(name, toolType); + } + + private final Class toolType; + private final String name; + private final ImmutableMap cmds; + + private static final HelpPrinter helpPrinter = new HelpPrinter(); + + private CliToolConfig(String name, Class toolType, Cmd[] cmds) { + this.name = name; + this.toolType = toolType; + ImmutableMap.Builder cmdsBuilder = ImmutableMap.builder(); + for (int i = 0; i < cmds.length; i++) { + cmdsBuilder.put(cmds[i].name, cmds[i]); + } + this.cmds = cmdsBuilder.build(); + } + + public boolean isSingle() { + return cmds.size() == 1; + } + + public Cmd single() { + assert isSingle() : "Requesting single command on a multi-command tool"; + return cmds.values().iterator().next(); + } + + public Class toolType() { + return toolType; + } + + public String name() { + return name; + } + + public Collection cmds() { + return cmds.values(); + } + + public Cmd cmd(String name) { + return cmds.get(name); + } + + public void printUsage(Terminal terminal) { + helpPrinter.print(this, terminal); + } + + public static class Builder { + + public static Cmd.Builder cmd(String name, Class cmdType) { + return new Cmd.Builder(name, cmdType); + } + + public static OptionBuilder option(String shortName, String longName) { + return new OptionBuilder(shortName, longName); + } + + private final Class toolType; + private final String name; + private Cmd[] cmds; + + private Builder(String name, Class toolType) { + this.name = name; + this.toolType = toolType; + } + + public Builder cmds(Cmd.Builder... cmds) { + this.cmds = new Cmd[cmds.length]; + for (int i = 0; i < cmds.length; i++) { + this.cmds[i] = cmds[i].build(); + } + return this; + } + + public Builder cmds(Cmd... cmds) { + this.cmds = cmds; + return this; + } + + public CliToolConfig build() { + return new CliToolConfig(name, toolType, cmds); + } + } + + public static class Cmd { + + private final String name; + private final Class cmdType; + private final Options options; + + private Cmd(String name, Class cmdType, Options options) { + this.name = name; + this.cmdType = cmdType; + this.options = options; + this.options.addOption(new OptionBuilder("h", "help").required(false).build()); + } + + public Class cmdType() { + return cmdType; + } + + public String name() { + return name; + } + + public Options options() { + return options; + } + + public void printUsage(Terminal terminal) { + helpPrinter.print(this, terminal); + } + + public static class Builder { + + private final String name; + private final Class cmdType; + private Options options = new Options(); + + private Builder(String name, Class cmdType) { + this.name = name; + this.cmdType = cmdType; + } + + public Builder options(OptionBuilder... optionBuilder) { + for (int i = 0; i < optionBuilder.length; i++) { + options.addOption(optionBuilder[i].build()); + } + return this; + } + + public Cmd build() { + return new Cmd(name, cmdType, options); + } + } + } + + public static class OptionBuilder { + + private final Option option; + + private OptionBuilder(String shortName, String longName) { + option = new Option(shortName, ""); + option.setLongOpt(longName); + option.setArgName(longName); + } + + public OptionBuilder required(boolean required) { + option.setRequired(required); + return this; + } + + public OptionBuilder hasArg(boolean optional) { + option.setOptionalArg(optional); + option.setArgs(1); + return this; + } + + public Option build() { + return option; + } + } +} diff --git a/src/main/java/org/elasticsearch/shield/support/cli/HelpPrinter.java b/src/main/java/org/elasticsearch/shield/support/cli/HelpPrinter.java new file mode 100644 index 00000000000..550d4daa41a --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/support/cli/HelpPrinter.java @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.support.cli; + +import com.google.common.base.Charsets; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * + */ +public class HelpPrinter { + + public void print(CliToolConfig config, Terminal terminal) { + URL url = config.toolType().getResource(config.name() + ".help"); + print(url, terminal); + } + + public void print(CliToolConfig.Cmd cmd, Terminal terminal) { + URL url = cmd.cmdType().getResource(cmd.name() + ".help"); + print(url, terminal); + } + + private static void print(URL url, Terminal terminal) { + terminal.println(); + try { + Path helpFile = Paths.get(url.toURI()); + for (String line : Files.readAllLines(helpFile, Charsets.UTF_8)) { + terminal.println(line); + } + } catch (IOException | URISyntaxException e) { + e.printStackTrace(terminal.writer()); + } + terminal.println(); + } +} diff --git a/src/main/java/org/elasticsearch/shield/support/cli/Terminal.java b/src/main/java/org/elasticsearch/shield/support/cli/Terminal.java new file mode 100644 index 00000000000..d31195ae50d --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/support/cli/Terminal.java @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.support.cli; + +import java.io.*; +import java.util.Locale; + +/** +* +*/ +public abstract class Terminal { + + public static final Terminal INSTANCE = ConsoleTerminal.supported() ? new ConsoleTerminal() : new SystemTerminal(); + + public abstract String readText(String text, Object... args); + + public abstract char[] readSecret(String text, Object... args); + + public abstract void println(); + + public abstract void println(String msg, Object... args); + + public abstract void print(String msg, Object... args); + + public abstract PrintWriter writer(); + + public static abstract class Base extends Terminal { + + @Override + public void println() { + println(""); + } + + @Override + public void println(String msg, Object... args) { + print(msg + System.lineSeparator(), args); + } + + } + + private static class ConsoleTerminal extends Base { + + final Console console = System.console(); + + static boolean supported() { + return System.console() != null; + } + + private ConsoleTerminal() { + } + + @Override + public void print(String msg, Object... args) { + console.printf(msg, args); + console.flush(); + } + + @Override + public String readText(String text, Object... args) { + return console.readLine(text, args); + } + + @Override + public char[] readSecret(String text, Object... args) { + return console.readPassword(text, args); + } + + @Override + public PrintWriter writer() { + return console.writer(); + } + + } + + private static class SystemTerminal extends Base { + + private final PrintWriter printWriter = new PrintWriter(System.out); + + @Override + public void print(String msg, Object... args) { + System.out.print(String.format(Locale.ROOT, msg, args)); + } + + @Override + public String readText(String text, Object... args) { + print(text, args); + BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); + try { + return reader.readLine(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @Override + public char[] readSecret(String text, Object... args) { + return readText(text, args).toCharArray(); + } + + @Override + public PrintWriter writer() { + return printWriter; + } + } +} diff --git a/src/main/resources/config/.roles.yml b/src/main/resources/config/.roles.yml deleted file mode 100644 index 15c0d64e507..00000000000 --- a/src/main/resources/config/.roles.yml +++ /dev/null @@ -1,13 +0,0 @@ -admin: - cluster: ALL - indices: - - *::ALL - -power_user: - cluster: MONITOR - indices: - - *::MONITOR,DATA_ACCESS - -user: - indices: - - *::READ,INDEX,MANAGE \ No newline at end of file diff --git a/src/main/resources/config/.users b/src/main/resources/config/.users deleted file mode 100644 index 0428fee08d8..00000000000 --- a/src/main/resources/config/.users +++ /dev/null @@ -1,3 +0,0 @@ -admin:{plain}changeme -poweruser:{plain)changeme -user:{plain}changeme \ No newline at end of file diff --git a/src/main/resources/config/.users_roles b/src/main/resources/config/.users_roles deleted file mode 100644 index ca370c710aa..00000000000 --- a/src/main/resources/config/.users_roles +++ /dev/null @@ -1,3 +0,0 @@ -admin:admin -poweruser:poweruser -user:user \ No newline at end of file diff --git a/src/main/resources/config/logging.yml b/src/main/resources/config/logging.yml deleted file mode 100644 index 382c21a6234..00000000000 --- a/src/main/resources/config/logging.yml +++ /dev/null @@ -1,12 +0,0 @@ -logger: - shield.audit.logfile: INFO, audit_file - -appender: - - audit_file: - type: dailyRollingFile - file: ${path.logs}/${cluster.name}.log - datePattern: "'.'yyyy-MM-dd" - layout: - type: pattern - conversionPattern: "[%d{ISO8601}] %m%n" diff --git a/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/esusers.help b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/esusers.help new file mode 100644 index 00000000000..4d5122d2b03 --- /dev/null +++ b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/esusers.help @@ -0,0 +1,22 @@ +SYNTAX: + + esusers + +DESCRIPTION: + + This tool manages all native security aspects in elasticsearch, saving + the administratorfrom needing to modify security related fiels manually. + This tool provides several commands for different security management + tasks + +COMMANDS: + + passwd Changes passwords for a native user + + useradd Adds a new native user to the system + + userdel Removes an existing native user from the system + +NOTES: + + [1] For usage help on specific commands please type "security -h" \ No newline at end of file diff --git a/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/passwd.help b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/passwd.help new file mode 100644 index 00000000000..3096abcafa0 --- /dev/null +++ b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/passwd.help @@ -0,0 +1,18 @@ +USAGE: + + esusers passwd [-p ] + +DESCRIPTION: + + The passwd command changes passwords for native user accounts. The tool + prompts twice for a replacement password. The second entry is compared + against the first and both are required to match in order for the + password to be changed. If non-default users file is used (a different + file location is configured in elasticsearch.yml) the appropriate file + will be resolved from the settings. + +OPTIONS: + + -h,--help Shows this message + + -p,--password The new password for the user \ No newline at end of file diff --git a/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/useradd.help b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/useradd.help new file mode 100644 index 00000000000..6021064e49b --- /dev/null +++ b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/useradd.help @@ -0,0 +1,21 @@ +USAGE: + + esusers useradd [-p ] [-r ] + +DESCRIPTION: + + Adds a native user to elasticsearch (via internal realm). The user will + be added to the users file and its roles will be added to the + users_roles file. If non-default files are used (different file + locations are configured in elasticsearch.yml) the appropriate files + will be resolved from the settings and the user and its roles will be + added to them. + +OPTIONS: + + -h,--help Shows this message + + -p,--password The user password + + -r,--roles Comma-separated list of the roles of the + user \ No newline at end of file diff --git a/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/userdel.help b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/userdel.help new file mode 100644 index 00000000000..c21b3508760 --- /dev/null +++ b/src/main/resources/org/elasticsearch/shield/authc/esusers/tool/userdel.help @@ -0,0 +1,16 @@ +USAGE: + + esusers userdel + +DESCRIPTION: + + Removes an existing native user from elasticsearch. The user will be + removed from the users file and its roles will be removed to the + users_roles file. If non-default files are used (different file + locations are configured in elasticsearch.yml) the appropriate files + will be resolved from the settings and the user and its roles will be + removed to them. + +OPTIONS: + + -h,--help Shows this message \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStoreTests.java b/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStoreTests.java index 35063f69c96..2bede7dde85 100644 --- a/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStoreTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserPasswdStoreTests.java @@ -62,7 +62,7 @@ public class FileUserPasswdStoreTests extends ElasticsearchTestCase { Settings settings = ImmutableSettings.builder() .put("watcher.interval", "2s") - .put("shield.authc.esusers.file.users", tmp.toAbsolutePath()) + .put("shield.authc.esusers.files.users", tmp.toAbsolutePath()) .build(); Environment env = new Environment(settings); diff --git a/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStoreTests.java b/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStoreTests.java index e9762299093..aa9e191a4b3 100644 --- a/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStoreTests.java +++ b/src/test/java/org/elasticsearch/shield/authc/esusers/FileUserRolesStoreTests.java @@ -58,7 +58,7 @@ public class FileUserRolesStoreTests extends ElasticsearchTestCase { Settings settings = ImmutableSettings.builder() .put("watcher.interval", "2s") - .put("shield.authc.esusers.file.users_roles", tmp.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", tmp.toAbsolutePath()) .build(); Environment env = new Environment(settings); diff --git a/src/test/java/org/elasticsearch/shield/authc/esusers/tool/ESUsersToolTests.java b/src/test/java/org/elasticsearch/shield/authc/esusers/tool/ESUsersToolTests.java new file mode 100644 index 00000000000..6c569ec6abd --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/authc/esusers/tool/ESUsersToolTests.java @@ -0,0 +1,368 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.authc.esusers.tool; + +import com.google.common.base.Charsets; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.shield.authc.support.Hasher; +import org.elasticsearch.shield.support.cli.CliToolTestCase; +import org.elasticsearch.shield.support.cli.CliTool; +import org.elasticsearch.shield.support.cli.Terminal; +import org.junit.Test; + +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import static org.hamcrest.Matchers.*; + +/** + * + */ +public class ESUsersToolTests extends CliToolTestCase { + + @Test + public void testUseradd_Parse_AllOptions() throws Exception { + ESUsersTool tool = new ESUsersTool(); + CliTool.Command command = tool.parse("useradd", args("username -p changeme -r r1,r2,r3")); + assertThat(command, instanceOf(ESUsersTool.Useradd.class)); + ESUsersTool.Useradd cmd = (ESUsersTool.Useradd) command; + assertThat(cmd.username, equalTo("username")); + assertThat(new String(cmd.passwd), equalTo("changeme")); + assertThat(cmd.roles, notNullValue()); + assertThat(cmd.roles, arrayContaining("r1", "r2", "r3")); + } + + @Test + public void testUseradd_Parse_NoUsername() throws Exception { + ESUsersTool tool = new ESUsersTool(); + CliTool.Command command = tool.parse("useradd", args("-p test123")); + assertThat(command, instanceOf(CliTool.Command.Exit.class)); + assertThat(((CliTool.Command.Exit) command).status(), is(CliTool.ExitStatus.USAGE)); + } + + @Test + public void testUseradd_Parse_NoPassword() throws Exception { + ESUsersTool tool = new ESUsersTool(new TerminalMock() { + @Override + public char[] readSecret(String text, Object... args) { + return "changeme".toCharArray(); + } + }); + CliTool.Command command = tool.parse("useradd", args("username")); + assertThat(command, instanceOf(ESUsersTool.Useradd.class)); + ESUsersTool.Useradd cmd = (ESUsersTool.Useradd) command; + assertThat(cmd.username, equalTo("username")); + assertThat(new String(cmd.passwd), equalTo("changeme")); + assertThat(cmd.roles, notNullValue()); + assertThat(cmd.roles.length, is(0)); + } + + @Test + public void testUseradd_Cmd_Create() throws Exception { + Path dir = Files.createTempDirectory(null); + Path users = dir.resolve("users"); + Path usersRoles = dir.resolve("users_roles"); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", usersRoles.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new TerminalMock(), "user1", "changeme".toCharArray(), "r1", "r2"); + + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.OK)); + + assertThat(Files.exists(users), is(true)); + List lines = Files.readAllLines(users, Charsets.UTF_8); + assertThat(lines.size(), is(1)); + // we can't just hash again and compare the lines, as every time we hash a new salt is generated + // instead we'll just verify the generated hash against the correct password. + String line = lines.get(0); + assertThat(line, startsWith("user1:")); + String hash = line.substring("user1:".length()); + assertThat(Hasher.HTPASSWD.verify("changeme".toCharArray(), hash.toCharArray()), is(true)); + + assertThat(Files.exists(usersRoles), is(true)); + lines = Files.readAllLines(usersRoles, Charsets.UTF_8); + assertThat(lines.size(), is(1)); + line = lines.get(0); + assertThat(line, equalTo("user1:r1,r2")); + } + + @Test + public void testUseradd_Cmd_Append() throws Exception { + Path users = Files.createTempFile(null, null); + Path usersRoles = Files.createTempFile(null, null); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", usersRoles.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + try (BufferedWriter writer = Files.newBufferedWriter(users, Charsets.UTF_8)) { + writer.write("user2:hash2"); + writer.flush(); + } + + try (BufferedWriter writer = Files.newBufferedWriter(usersRoles, Charsets.UTF_8)) { + writer.write("user2:r3,r4"); + writer.flush(); + } + + ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new TerminalMock(), "user1", "changeme".toCharArray(), "r1", "r2"); + + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.OK)); + + assertThat(Files.exists(users), is(true)); + List lines = Files.readAllLines(users, Charsets.UTF_8); + assertThat(lines.size(), is(2)); + assertThat(lines.get(0), equalTo("user2:hash2")); + // we can't just hash again and compare the lines, as every time we hash a new salt is generated + // instead we'll just verify the generated hash against the correct password. + String line = lines.get(1); + assertThat(line, startsWith("user1:")); + String hash = line.substring("user1:".length()); + assertThat(Hasher.HTPASSWD.verify("changeme".toCharArray(), hash.toCharArray()), is(true)); + + assertThat(Files.exists(usersRoles), is(true)); + lines = Files.readAllLines(usersRoles, Charsets.UTF_8); + assertThat(lines.size(), is(2)); + assertThat(lines.get(0), equalTo("user2:r3,r4")); + line = lines.get(1); + assertThat(line, equalTo("user1:r1,r2")); + } + + @Test + public void testUseradd_Cmd_Append_UserAlreadyExists() throws Exception { + Path users = Files.createTempFile(null, null); + Path usersRoles = Files.createTempFile(null, null); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", usersRoles.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + try (BufferedWriter writer = Files.newBufferedWriter(users, Charsets.UTF_8)) { + writer.write("user1:hash1"); + writer.flush(); + } + + ESUsersTool.Useradd cmd = new ESUsersTool.Useradd(new TerminalMock(), "user1", "changeme".toCharArray(), "r1", "r2"); + + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.CODE_ERROR)); + } + + @Test + public void testUserdel_Parse() throws Exception { + ESUsersTool tool = new ESUsersTool(); + CliTool.Command command = tool.parse("userdel", args("username")); + assertThat(command, instanceOf(ESUsersTool.Userdel.class)); + ESUsersTool.Userdel userdel = (ESUsersTool.Userdel) command; + assertThat(userdel.username, equalTo("username")); + } + + @Test + public void testUserdel_Parse_MissingUsername() throws Exception { + ESUsersTool tool = new ESUsersTool(); + CliTool.Command command = tool.parse("userdel", args(null)); + assertThat(command, instanceOf(ESUsersTool.Command.Exit.class)); + ESUsersTool.Command.Exit exit = (ESUsersTool.Command.Exit) command; + assertThat(exit.status(), equalTo(CliTool.ExitStatus.USAGE)); + } + + @Test + public void testUserdel_Cmd() throws Exception { + Path users = Files.createTempFile(null, null); + Path usersRoles = Files.createTempFile(null, null); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", usersRoles.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + try (BufferedWriter writer = Files.newBufferedWriter(users, Charsets.UTF_8)) { + writer.write("user1:hash2"); + writer.flush(); + } + + try (BufferedWriter writer = Files.newBufferedWriter(usersRoles, Charsets.UTF_8)) { + writer.write("user1:r3,r4"); + writer.flush(); + } + + ESUsersTool.Userdel cmd = new ESUsersTool.Userdel(new TerminalMock(), "user1"); + + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.OK)); + + assertThat(Files.exists(users), is(true)); + List lines = Files.readAllLines(users, Charsets.UTF_8); + assertThat(lines.size(), is(0)); + + assertThat(Files.exists(usersRoles), is(true)); + lines = Files.readAllLines(usersRoles, Charsets.UTF_8); + assertThat(lines.size(), is(0)); + } + + @Test + public void testUserdel_Cmd_MissingUser() throws Exception { + Path users = Files.createTempFile(null, null); + Path usersRoles = Files.createTempFile(null, null); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", usersRoles.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + try (BufferedWriter writer = Files.newBufferedWriter(users, Charsets.UTF_8)) { + writer.write("user1:hash2"); + writer.flush(); + } + + try (BufferedWriter writer = Files.newBufferedWriter(usersRoles, Charsets.UTF_8)) { + writer.write("user1:r3,r4"); + writer.flush(); + } + + ESUsersTool.Userdel cmd = new ESUsersTool.Userdel(new TerminalMock(), "user2"); + + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.OK)); + + assertThat(Files.exists(users), is(true)); + List lines = Files.readAllLines(users, Charsets.UTF_8); + assertThat(lines.size(), is(1)); + + assertThat(Files.exists(usersRoles), is(true)); + lines = Files.readAllLines(usersRoles, Charsets.UTF_8); + assertThat(lines.size(), is(1)); + } + + @Test + public void testUserdel_Cmd_MissingFiles() throws Exception { + Path dir = Files.createTempDirectory(null); + Path users = dir.resolve("users"); + Path usersRoles = dir.resolve("users_roles"); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .put("shield.authc.esusers.files.users_roles", usersRoles.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + ESUsersTool.Userdel cmd = new ESUsersTool.Userdel(new TerminalMock(), "user2"); + + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.OK)); + + assertThat(Files.exists(users), is(false)); + assertThat(Files.exists(usersRoles), is(false)); + } + + @Test + public void testPasswd_Parse_AllOptions() throws Exception { + ESUsersTool tool = new ESUsersTool(); + CliTool.Command command = tool.parse("passwd", args("user1 -p changeme")); + assertThat(command, instanceOf(ESUsersTool.Passwd.class)); + ESUsersTool.Passwd cmd = (ESUsersTool.Passwd) command; + assertThat(cmd.username, equalTo("user1")); + assertThat(new String(cmd.passwd), equalTo("changeme")); + } + + @Test + public void testPasswd_Parse_MissingUsername() throws Exception { + ESUsersTool tool = new ESUsersTool(); + CliTool.Command command = tool.parse("passwd", args("-p changeme")); + assertThat(command, instanceOf(ESUsersTool.Command.Exit.class)); + ESUsersTool.Command.Exit cmd = (ESUsersTool.Command.Exit) command; + assertThat(cmd.status(), is(CliTool.ExitStatus.USAGE)); + } + + @Test + public void testPasswd_Parse_MissingPassword() throws Exception { + final AtomicReference secretRequested = new AtomicReference<>(false); + Terminal terminal = new TerminalMock() { + @Override + public char[] readSecret(String text, Object... args) { + secretRequested.set(true); + return "changeme".toCharArray(); + } + }; + ESUsersTool tool = new ESUsersTool(terminal); + CliTool.Command command = tool.parse("passwd", args("user1")); + assertThat(command, instanceOf(ESUsersTool.Passwd.class)); + ESUsersTool.Passwd cmd = (ESUsersTool.Passwd) command; + assertThat(cmd.username, equalTo("user1")); + assertThat(new String(cmd.passwd), equalTo("changeme")); + assertThat(secretRequested.get(), is(true)); + } + + @Test + public void testPasswd_Cmd() throws Exception { + Path users = Files.createTempFile(null, null); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + try (BufferedWriter writer = Files.newBufferedWriter(users, Charsets.UTF_8)) { + writer.write("user1:hash2"); + writer.flush(); + } + + ESUsersTool.Passwd cmd = new ESUsersTool.Passwd(new TerminalMock(), "user1", "changeme".toCharArray()); + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.OK)); + + List lines = Files.readAllLines(users, Charsets.UTF_8); + assertThat(lines.size(), is(1)); + // we can't just hash again and compare the lines, as every time we hash a new salt is generated + // instead we'll just verify the generated hash against the correct password. + String line = lines.get(0); + assertThat(line, startsWith("user1:")); + String hash = line.substring("user1:".length()); + assertThat(Hasher.HTPASSWD.verify("changeme".toCharArray(), hash.toCharArray()), is(true)); + } + + @Test + public void testPasswd_Cmd_UnknownUser() throws Exception { + Path users = Files.createTempFile(null, null); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + try (BufferedWriter writer = Files.newBufferedWriter(users, Charsets.UTF_8)) { + writer.write("user1:hash2"); + writer.flush(); + } + + ESUsersTool.Passwd cmd = new ESUsersTool.Passwd(new TerminalMock(), "user2", "changeme".toCharArray()); + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.NO_USER)); + } + + @Test + public void testPasswd_Cmd_MissingFiles() throws Exception { + Path dir = Files.createTempDirectory(null); + Path users = dir.resolve("users"); + Settings settings = ImmutableSettings.builder() + .put("shield.authc.esusers.files.users", users.toAbsolutePath()) + .build(); + Environment env = new Environment(settings); + + ESUsersTool.Passwd cmd = new ESUsersTool.Passwd(new TerminalMock(), "user2", "changeme".toCharArray()); + CliTool.ExitStatus status = cmd.execute(settings, env); + assertThat(status, is(CliTool.ExitStatus.NO_USER)); + } +} diff --git a/src/test/java/org/elasticsearch/shield/support/cli/CliToolTestCase.java b/src/test/java/org/elasticsearch/shield/support/cli/CliToolTestCase.java new file mode 100644 index 00000000000..df46856d459 --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/support/cli/CliToolTestCase.java @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.support.cli; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.test.ElasticsearchTestCase; + +import java.io.PrintWriter; + +/** + * + */ +public class CliToolTestCase extends ElasticsearchTestCase { + + protected static String[] args(String command) { + if (!Strings.hasLength(command)) { + return Strings.EMPTY_ARRAY; + } + return command.split("\\s+"); + } + + public static class TerminalMock extends Terminal { + + @Override + public void println() { + } + + @Override + public void println(String msg, Object... args) { + } + + @Override + public String readText(String text, Object... args) { + return null; + } + + @Override + public char[] readSecret(String text, Object... args) { + return new char[0]; + } + + @Override + public void print(String msg, Object... args) { + + } + + @Override + public PrintWriter writer() { + return null; + } + } + +} diff --git a/src/test/java/org/elasticsearch/shield/support/cli/CliToolTests.java b/src/test/java/org/elasticsearch/shield/support/cli/CliToolTests.java new file mode 100644 index 00000000000..1a086c34b58 --- /dev/null +++ b/src/test/java/org/elasticsearch/shield/support/cli/CliToolTests.java @@ -0,0 +1,285 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.shield.support.cli; + +import com.google.common.collect.ImmutableMap; +import org.apache.commons.cli.CommandLine; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import static org.elasticsearch.shield.support.cli.CliToolConfig.Builder.cmd; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +/** + * + */ +public class CliToolTests extends CliToolTestCase { + + @Test + public void testOK() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) { + executed.set(true); + return CliTool.ExitStatus.OK; + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertThat(executed.get(), is(true)); + assertThat(status, is(CliTool.ExitStatus.OK.status())); + } + + @Test + public void testUsageError() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) { + executed.set(true); + return CliTool.ExitStatus.USAGE; + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertThat(executed.get(), is(true)); + assertThat(status, is(CliTool.ExitStatus.USAGE.status())); + } + + @Test + public void testIOError() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed.set(true); + throw new IOException("io error"); + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertThat(executed.get(), is(true)); + assertThat(status, is(CliTool.ExitStatus.IO_ERROR.status())); + } + + @Test + public void testCodeError() throws Exception { + Terminal terminal = new TerminalMock(); + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed.set(true); + throw new Exception("random error"); + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(); + assertThat(executed.get(), is(true)); + assertThat(status, is(CliTool.ExitStatus.CODE_ERROR.status())); + } + + public void testMultiCommand() { + Terminal terminal = new TerminalMock(); + int count = randomIntBetween(2, 7); + final AtomicReference[] executed = new AtomicReference[count]; + for (int i = 0; i < executed.length; i++) { + executed[i] = new AtomicReference<>(false); + } + NamedCommand[] cmds = new NamedCommand[count]; + for (int i = 0; i < count; i++) { + final int index = i; + cmds[i] = new NamedCommand("cmd" + index, terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed[index].set(true); + return CliTool.ExitStatus.OK; + } + }; + } + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int cmdIndex = randomIntBetween(0, count-1); + int status = tool.execute("cmd" + cmdIndex); + assertThat(status, is(CliTool.ExitStatus.OK.status())); + for (int i = 0; i < executed.length; i++) { + assertThat(executed[i].get(), is(i == cmdIndex)); + } + } + + public void testMultiCommand_UnknownCommand() { + Terminal terminal = new TerminalMock(); + int count = randomIntBetween(2, 7); + final AtomicReference[] executed = new AtomicReference[count]; + for (int i = 0; i < executed.length; i++) { + executed[i] = new AtomicReference<>(false); + } + NamedCommand[] cmds = new NamedCommand[count]; + for (int i = 0; i < count; i++) { + final int index = i; + cmds[i] = new NamedCommand("cmd" + index, terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed[index].set(true); + return CliTool.ExitStatus.OK; + } + }; + } + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int status = tool.execute("cmd" + count); // "cmd" + count doesn't exist + assertThat(status, is(CliTool.ExitStatus.USAGE.status())); + for (int i = 0; i < executed.length; i++) { + assertThat(executed[i].get(), is(false)); + } + } + + @Test + public void testSingleCommand_ToolHelp() throws Exception { + final AtomicReference helpWritten = new AtomicReference<>(false); + Terminal terminal = new TerminalMock() { + @Override + public void println(String msg, Object... args) { + assertThat(msg, equalTo("cmd1 help")); + helpWritten.set(true); + } + }; + final AtomicReference executed = new AtomicReference<>(false); + final NamedCommand cmd = new NamedCommand("cmd1", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + executed.set(true); + throw new IOException("io error"); + } + }; + SingleCmdTool tool = new SingleCmdTool("tool", terminal, cmd); + int status = tool.execute(args("-h")); + assertThat(status, is(CliTool.ExitStatus.OK.status())); + assertThat(helpWritten.get(), is(true)); + } + + public void testMultiCommand_ToolHelp() { + final AtomicReference helpWritten = new AtomicReference<>(false); + Terminal terminal = new TerminalMock() { + @Override + public void println(String msg, Object... args) { + assertThat(msg, equalTo("tool help")); + helpWritten.set(true); + } + }; + NamedCommand[] cmds = new NamedCommand[2]; + cmds[0] = new NamedCommand("cmd0", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + cmds[1] = new NamedCommand("cmd1", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int status = tool.execute(args("-h")); + assertThat(status, is(CliTool.ExitStatus.OK.status())); + assertThat(helpWritten.get(), is(true)); + } + + public void testMultiCommand_CmdHelp() { + final AtomicReference helpWritten = new AtomicReference<>(false); + Terminal terminal = new TerminalMock() { + @Override + public void println(String msg, Object... args) { + assertThat(msg, equalTo("cmd1 help")); + helpWritten.set(true); + } + }; + NamedCommand[] cmds = new NamedCommand[2]; + cmds[0] = new NamedCommand("cmd0", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + cmds[1] = new NamedCommand("cmd1", terminal) { + @Override + public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception { + return CliTool.ExitStatus.OK; + } + }; + MultiCmdTool tool = new MultiCmdTool("tool", terminal, cmds); + int status = tool.execute(args("cmd1 -h")); + assertThat(status, is(CliTool.ExitStatus.OK.status())); + assertThat(helpWritten.get(), is(true)); + } + + private static class SingleCmdTool extends CliTool { + + private final Command command; + + private SingleCmdTool(String name, Terminal terminal, NamedCommand command) { + super(CliToolConfig.config(name, SingleCmdTool.class) + .cmds(cmd(command.name, command.getClass())) + .build(), terminal); + this.command = command; + } + + @Override + protected Command parse(String cmdName, CommandLine cli) throws Exception { + return command; + } + } + + private static class MultiCmdTool extends CliTool { + + private final Map commands; + + private MultiCmdTool(String name, Terminal terminal, NamedCommand... commands) { + super(CliToolConfig.config(name, MultiCmdTool.class) + .cmds(cmds(commands)) + .build(), terminal); + ImmutableMap.Builder commandByName = ImmutableMap.builder(); + for (int i = 0; i < commands.length; i++) { + commandByName.put(commands[i].name, commands[i]); + } + this.commands = commandByName.build(); + } + + @Override + protected Command parse(String cmdName, CommandLine cli) throws Exception { + return commands.get(cmdName); + } + + private static CliToolConfig.Cmd[] cmds(NamedCommand... commands) { + CliToolConfig.Cmd[] cmds = new CliToolConfig.Cmd[commands.length]; + for (int i = 0; i < commands.length; i++) { + cmds[i] = cmd(commands[i].name, commands[i].getClass()).build(); + } + return cmds; + } + } + + private static abstract class NamedCommand extends CliTool.Command { + + private final String name; + + private NamedCommand(String name, Terminal terminal) { + super(terminal); + this.name = name; + } + } + + +} diff --git a/src/test/resources/org/elasticsearch/shield/support/cli/cmd1.help b/src/test/resources/org/elasticsearch/shield/support/cli/cmd1.help new file mode 100644 index 00000000000..d083e3a6534 --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/support/cli/cmd1.help @@ -0,0 +1 @@ +cmd1 help \ No newline at end of file diff --git a/src/test/resources/org/elasticsearch/shield/support/cli/tool.help b/src/test/resources/org/elasticsearch/shield/support/cli/tool.help new file mode 100644 index 00000000000..023b1accdff --- /dev/null +++ b/src/test/resources/org/elasticsearch/shield/support/cli/tool.help @@ -0,0 +1 @@ +tool help \ No newline at end of file