Added more unit tests, re-implemented & added tests for ESUsersTool

- Added CliTool infrastructure (should eventually be moved to core and removed from this repo)

Original commit: elastic/x-pack-elasticsearch@ba498163f5
This commit is contained in:
uboness 2014-07-21 04:37:22 +02:00
parent f727e29066
commit 9b3160b7ac
25 changed files with 1594 additions and 342 deletions

View File

@ -301,7 +301,7 @@
<excludes>
<exclude>jsr166e/**</exclude>
<!-- start excludes for valid system-out -->
<exclude>org/elasticsearch/shield/support/CmdLineTool*</exclude>
<exclude>org/elasticsearch/shield/support/cli/Terminal*</exclude>
<exclude>org/elasticsearch/common/logging/log4j/ConsoleAppender*</exclude>
<exclude>org/elasticsearch/plugins/PluginManager.class</exclude>
<exclude>org/elasticsearch/bootstrap/Bootstrap.class</exclude>

View File

@ -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<String, char[]> 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<String, char[]> 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);

View File

@ -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<String, String[]> 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<String, String[]> 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");

View File

@ -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<String, char[]> 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<String, String[]> 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<String, char[]> 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<String, String[]> 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<String, char[]> 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;
}
}
}

View File

@ -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<String, char[]> 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<String, String[]> 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);
}
}

View File

@ -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<Settings, Environment> 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;
}
}
}

View File

@ -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<Settings, Environment> 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;
}
}
}
}

View File

@ -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<? extends CliTool> toolType) {
return new Builder(name, toolType);
}
private final Class<? extends CliTool> toolType;
private final String name;
private final ImmutableMap<String, Cmd> cmds;
private static final HelpPrinter helpPrinter = new HelpPrinter();
private CliToolConfig(String name, Class<? extends CliTool> toolType, Cmd[] cmds) {
this.name = name;
this.toolType = toolType;
ImmutableMap.Builder<String, Cmd> 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<? extends CliTool> toolType() {
return toolType;
}
public String name() {
return name;
}
public Collection<Cmd> 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<? extends CliTool.Command> cmdType) {
return new Cmd.Builder(name, cmdType);
}
public static OptionBuilder option(String shortName, String longName) {
return new OptionBuilder(shortName, longName);
}
private final Class<? extends CliTool> toolType;
private final String name;
private Cmd[] cmds;
private Builder(String name, Class<? extends CliTool> 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<? extends CliTool.Command> cmdType;
private final Options options;
private Cmd(String name, Class<? extends CliTool.Command> cmdType, Options options) {
this.name = name;
this.cmdType = cmdType;
this.options = options;
this.options.addOption(new OptionBuilder("h", "help").required(false).build());
}
public Class<? extends CliTool.Command> 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<? extends CliTool.Command> cmdType;
private Options options = new Options();
private Builder(String name, Class<? extends CliTool.Command> 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;
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -1,13 +0,0 @@
admin:
cluster: ALL
indices:
- *::ALL
power_user:
cluster: MONITOR
indices:
- *::MONITOR,DATA_ACCESS
user:
indices:
- *::READ,INDEX,MANAGE

View File

@ -1,3 +0,0 @@
admin:{plain}changeme
poweruser:{plain)changeme
user:{plain}changeme

View File

@ -1,3 +0,0 @@
admin:admin
poweruser:poweruser
user:user

View File

@ -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"

View File

@ -0,0 +1,22 @@
SYNTAX:
esusers <command>
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 <command> -h"

View File

@ -0,0 +1,18 @@
USAGE:
esusers passwd <username> [-p <password>]
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 <password> The new password for the user

View File

@ -0,0 +1,21 @@
USAGE:
esusers useradd <username> [-p <password>] [-r <roles>]
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 <password> The user password
-r,--roles <roles> Comma-separated list of the roles of the
user

View File

@ -0,0 +1,16 @@
USAGE:
esusers userdel <username>
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

View File

@ -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);

View File

@ -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);

View File

@ -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<String> 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<String> 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<String> 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<String> 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<Boolean> 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<String> 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));
}
}

View File

@ -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;
}
}
}

View File

@ -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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean>[] 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<Boolean>[] 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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<String, Command> commands;
private MultiCmdTool(String name, Terminal terminal, NamedCommand... commands) {
super(CliToolConfig.config(name, MultiCmdTool.class)
.cmds(cmds(commands))
.build(), terminal);
ImmutableMap.Builder<String, Command> 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;
}
}
}

View File

@ -0,0 +1 @@
cmd1 help

View File

@ -0,0 +1 @@
tool help