diff options
author | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2019-05-31 16:18:56 +0200 |
---|---|---|
committer | Bjørn Christian Seime <bjorncs@verizonmedia.com> | 2019-05-31 16:18:56 +0200 |
commit | 628432bf640ef1c20c39718dcbb508e4a56bf8b8 (patch) | |
tree | 48142da36f0a1e716424dcfe5aaa27c9c963df8a /security-tools/src | |
parent | b0c953ea8f0c6ad4e757797001de85639b3ccdda (diff) |
Add implementation of the 'vespa-security-env' tool
Diffstat (limited to 'security-tools/src')
8 files changed, 379 insertions, 1 deletions
diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java new file mode 100644 index 00000000000..f3ec73236c4 --- /dev/null +++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/CliOptions.java @@ -0,0 +1,69 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.security.tool.securityenv; + +import com.yahoo.security.tls.TransportSecurityUtils; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.Arrays; + +import static java.util.stream.Collectors.joining; + +/** + * Defines the program's command line parameters. + * + * @author bjorncs + */ +class CliOptions { + static final String SHELL_OPTION = "shell"; + static final String HELP_OPTION = "help"; + + private static final Options OPTIONS = new Options() + .addOption( + Option.builder("s") + .longOpt(SHELL_OPTION) + .hasArg(true) + .required(false) + .desc(String.format("Shell type. Shell type is auto-detected if option not present. Valid values: %s.", + Arrays.stream(UnixShell.values()) + .map(shell -> String.format("'%s'", shell.configName())) + .collect(joining(", ", "[", "]")))) + .build()) + .addOption(Option.builder("h") + .longOpt(HELP_OPTION) + .hasArg(false) + .required(false) + .desc("Show help") + .build()); + + static CommandLine parseCliArguments(String[] cliArgs) throws ParseException { + CommandLineParser parser = new DefaultParser(); + return parser.parse(OPTIONS, cliArgs); + } + + static void printHelp(PrintStream out) { + HelpFormatter formatter = new HelpFormatter(); + PrintWriter writer = new PrintWriter(out); + formatter.printHelp( + writer, + formatter.getWidth(), + "vespa-security-env <options>", + String.format("Generates shell commands that defines environments variables based on the content of %s.", + TransportSecurityUtils.CONFIG_FILE_ENVIRONMENT_VARIABLE), + OPTIONS, + formatter.getLeftPadding(), + formatter.getDescPadding(), + String.format("The output may include the following variables:\n%s\n", + Arrays.stream(OutputVariable.values()) + .map(variable -> String.format(" - '%s': %s", variable.variableName(), variable.description())) + .collect(joining("\n")))); + writer.flush(); + } +} diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java index f57575b406a..74c08c2d602 100644 --- a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java +++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/Main.java @@ -1,11 +1,87 @@ // Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.security.tool.securityenv; +import com.yahoo.security.tls.MixedMode; +import com.yahoo.security.tls.TransportSecurityOptions; +import com.yahoo.security.tls.TransportSecurityUtils; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.ParseException; + +import java.io.PrintStream; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import static com.yahoo.vespa.security.tool.securityenv.CliOptions.HELP_OPTION; +import static com.yahoo.vespa.security.tool.securityenv.CliOptions.SHELL_OPTION; + /** + * Implementation of the 'vespa-security-env' command line utility. + * * @author bjorncs */ public class Main { + + private final PrintStream stdOut; + private final PrintStream stdError; + + Main(PrintStream stdOut, PrintStream stdError) { + this.stdOut = stdOut; + this.stdError = stdError; + } + public static void main(String[] args) { - System.out.println("TODO implementation"); + Main program = new Main(System.out, System.err); + int statusCode = program.execute(args, System.getenv()); + System.exit(statusCode); + } + + int execute(String[] cliArgs, Map<String, String> envVars) { + boolean debugMode = envVars.containsKey("VESPA_DEBUG"); + try { + CommandLine arguments = CliOptions.parseCliArguments(cliArgs); + if (arguments.hasOption(HELP_OPTION)) { + CliOptions.printHelp(stdOut); + return 0; + } + UnixShell shell = arguments.hasOption(SHELL_OPTION) + ? UnixShell.fromConfigName(arguments.getOptionValue(SHELL_OPTION)) + : UnixShell.detect(envVars.get("SHELL")); + + Optional<TransportSecurityOptions> options = TransportSecurityUtils.getOptions(envVars); + if (options.isEmpty()) { + return 0; + } + Map<String, String> outputVariables = new TreeMap<>(); + options.get().getCaCertificatesFile() + .ifPresent(caCertFile -> addOutputVariable(outputVariables, OutputVariable.CA_CERTIFICATE, caCertFile.toString())); + MixedMode mixedMode = TransportSecurityUtils.getInsecureMixedMode(envVars); + if (mixedMode != MixedMode.PLAINTEXT_CLIENT_MIXED_SERVER) { + options.get().getCertificatesFile() + .ifPresent(certificateFile -> addOutputVariable(outputVariables, OutputVariable.CERTIFICATE, certificateFile.toString())); + options.get().getPrivateKeyFile() + .ifPresent(privateKeyFile -> addOutputVariable(outputVariables, OutputVariable.PRIVATE_KEY, privateKeyFile.toString())); + } + shell.writeOutputVariables(stdOut, outputVariables); + return 0; + } catch (ParseException e) { + return handleException("Failed to parse command line arguments: " + e.getMessage(), e, debugMode); + } catch (IllegalArgumentException e) { + return handleException("Invalid command line arguments: " + e.getMessage(), e, debugMode); + } catch (Exception e) { + return handleException("Failed to generate security environment variables: " + e.getMessage(), e, debugMode); + } + } + + private static void addOutputVariable(Map<String, String> outputVariables, OutputVariable variable, String value) { + outputVariables.put(variable.variableName(), value); + } + + private int handleException(String message, Exception exception, boolean debugMode) { + stdError.println(message); + if (debugMode) { + exception.printStackTrace(stdError); + } + return 1; } } diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java new file mode 100644 index 00000000000..9cd4cc1fc67 --- /dev/null +++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/OutputVariable.java @@ -0,0 +1,29 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.security.tool.securityenv; + +/** + * Define the possible environment variables that the program may output. + * + * @author bjorncs + */ +enum OutputVariable { + CA_CERTIFICATE("VESPA_TLS_CA_CERT", "Path to CA certificates file"), + CERTIFICATE("VESPA_TLS_CERT", "Path to certificate file"), + PRIVATE_KEY("VESPA_TLS_PRIVATE_KEY", "Path to private key file"); + + private final String variableName; + private final String description; + + OutputVariable(String variableName, String description) { + this.variableName = variableName; + this.description = description; + } + + String variableName() { + return variableName; + } + + String description() { + return description; + } +} diff --git a/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java new file mode 100644 index 00000000000..1b4a9696c69 --- /dev/null +++ b/security-tools/src/main/java/com/yahoo/vespa/security/tool/securityenv/UnixShell.java @@ -0,0 +1,74 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.security.tool.securityenv; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * Definition of some unix shell variants and how to export environments variable for those supported. + * The output format is inspired by ssh-agent's output. + * + * @author bjorncs + */ +enum UnixShell { + BOURNE("bourne", List.of("bash", "sh")) { + @Override + void writeOutputVariables(PrintStream out, Map<String, String> outputVariables) { + outputVariables.forEach((name, value) -> { + out.print(name); + out.print('='); + out.print(value); // note: value is assumed to need no escaping + out.print("; export "); + out.print(name); + out.println(';'); + }); + } + }, + CSHELL("cshell", List.of("csh", "fish")) { + @Override + void writeOutputVariables(PrintStream out, Map<String, String> outputVariables) { + outputVariables.forEach((name, value) -> { + out.print("setenv "); + out.print(name); + out.print(' '); + out.print(value); // note: value is assumed to need no escaping + out.println(';'); + }); + } + }; + + private static final UnixShell DEFAULT = BOURNE; + + private final String configName; + private final List<String> knownShellBinaries; + + UnixShell(String configName, List<String> knownShellBinaries) { + this.configName = configName; + this.knownShellBinaries = knownShellBinaries; + } + + abstract void writeOutputVariables(PrintStream out, Map<String, String> outputVariables); + + String configName() { + return configName; + } + + static UnixShell fromConfigName(String configName) { + return Arrays.stream(values()) + .filter(shell -> shell.configName.equals(configName)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("Unknown shell: " + configName)); + } + + static UnixShell detect(String shellEnvVariable) { + if (shellEnvVariable == null || shellEnvVariable.isEmpty()) return DEFAULT; + int lastSlash = shellEnvVariable.lastIndexOf('/'); + String shellName = lastSlash != -1 ? shellEnvVariable.substring(lastSlash + 1) : shellEnvVariable; + return Arrays.stream(values()) + .filter(shell -> shell.knownShellBinaries.contains(shellName)) + .findAny() + .orElse(DEFAULT); + } +} diff --git a/security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java b/security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java new file mode 100644 index 00000000000..6b25c2a2bce --- /dev/null +++ b/security-tools/src/test/java/com/yahoo/vespa/security/tool/securityenv/MainTest.java @@ -0,0 +1,114 @@ +// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.vespa.security.tool.securityenv; + +import com.yahoo.security.tls.MixedMode; +import com.yahoo.security.tls.TransportSecurityOptions; +import com.yahoo.security.tls.TransportSecurityUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author bjorncs + */ +public class MainTest { + + private final ByteArrayOutputStream stdOutBytes = new ByteArrayOutputStream(); + private final ByteArrayOutputStream stdErrBytes = new ByteArrayOutputStream(); + private final PrintStream stdOut = new PrintStream(stdOutBytes); + private final PrintStream stdError = new PrintStream(stdErrBytes); + + @Rule + public TemporaryFolder tmpFolder = new TemporaryFolder(); + + @Test + public void prints_help_page_on_help_option() throws IOException { + int exitCode = runMain(List.of("--help"), Map.of()); + assertThat(exitCode).isEqualTo(0); + assertThat(stdOut()).isEqualTo(readTestResource("expected-help-output.txt")); + } + + @Test + public void prints_no_output_when_no_security_config() { + int exitCode = runMain(List.of(), Map.of()); + assertThat(exitCode).isEqualTo(0); + assertThat(stdErr()).isEmpty(); + } + + @Test + public void prints_security_variables_with_specified_shell() throws IOException { + Path configFile = generateConfigFile(); + Map<String, String> env = Map.of(TransportSecurityUtils.CONFIG_FILE_ENVIRONMENT_VARIABLE, configFile.toString()); + int exitCode = runMain(List.of(), env); + assertThat(exitCode).isEqualTo(0); + assertThat(stdOut()).isEqualTo(readTestResource("bash-output.txt")); + } + + @Test + public void prints_security_variables_with_auto_detected_shell() throws IOException { + Path configFile = generateConfigFile(); + Map<String, String> env = Map.of( + TransportSecurityUtils.CONFIG_FILE_ENVIRONMENT_VARIABLE, configFile.toString(), + TransportSecurityUtils.INSECURE_MIXED_MODE_ENVIRONMENT_VARIABLE, MixedMode.TLS_CLIENT_MIXED_SERVER.configValue(), + "SHELL", "/usr/local/bin/fish"); + int exitCode = runMain(List.of(), env); + assertThat(exitCode).isEqualTo(0); + assertThat(stdOut()).isEqualTo(readTestResource("csh-output.txt")); + } + + + @Test + public void prints_error_message_on_unknown_shell_name() { + int exitCode = runMain(List.of("--shell", "invalid-shell-name"), Map.of()); + assertThat(exitCode).isEqualTo(1); + assertThat(stdErr()).isEqualTo("Invalid command line arguments: Unknown shell: invalid-shell-name\n"); + } + + @Test + public void prints_error_message_on_unknown_command_line_parameter() { + int exitCode = runMain(List.of("--unknown-parameter"), Map.of()); + assertThat(exitCode).isEqualTo(1); + assertThat(stdErr()).isEqualTo("Failed to parse command line arguments: Unrecognized option: --unknown-parameter\n"); + } + + private int runMain(List<String> args, Map<String, String> env) { + return new Main(stdOut, stdError).execute(args.toArray(new String[0]), env); + } + + private String stdOut() { + stdOut.flush(); + return stdOutBytes.toString(); + } + + private String stdErr() { + stdError.flush(); + return stdErrBytes.toString(); + } + + private static String readTestResource(String fileName) throws IOException { + return Files.readString(Paths.get(MainTest.class.getResource('/' + fileName).getFile())); + } + + private Path generateConfigFile() throws IOException { + TransportSecurityOptions options = new TransportSecurityOptions.Builder() + .withCertificates(Paths.get("/path/to/certificate"), Paths.get("/path/to/key")) + .withCaCertificates(Paths.get("/path/to/cacerts")) + .build(); + Path configFile = tmpFolder.newFile().toPath(); + options.toJsonFile(configFile); + return configFile; + } + +}
\ No newline at end of file diff --git a/security-tools/src/test/resources/bash-output.txt b/security-tools/src/test/resources/bash-output.txt new file mode 100644 index 00000000000..9d603883953 --- /dev/null +++ b/security-tools/src/test/resources/bash-output.txt @@ -0,0 +1,3 @@ +VESPA_TLS_CA_CERT=/path/to/cacerts; export VESPA_TLS_CA_CERT; +VESPA_TLS_CERT=/path/to/certificate; export VESPA_TLS_CERT; +VESPA_TLS_PRIVATE_KEY=/path/to/key; export VESPA_TLS_PRIVATE_KEY; diff --git a/security-tools/src/test/resources/csh-output.txt b/security-tools/src/test/resources/csh-output.txt new file mode 100644 index 00000000000..6db3e613d90 --- /dev/null +++ b/security-tools/src/test/resources/csh-output.txt @@ -0,0 +1,3 @@ +setenv VESPA_TLS_CA_CERT /path/to/cacerts; +setenv VESPA_TLS_CERT /path/to/certificate; +setenv VESPA_TLS_PRIVATE_KEY /path/to/key; diff --git a/security-tools/src/test/resources/expected-help-output.txt b/security-tools/src/test/resources/expected-help-output.txt new file mode 100644 index 00000000000..e16f1b1dab0 --- /dev/null +++ b/security-tools/src/test/resources/expected-help-output.txt @@ -0,0 +1,10 @@ +usage: vespa-security-env <options> +Generates shell commands that defines environments variables based on the +content of VESPA_TLS_CONFIG_FILE. + -h,--help Show help + -s,--shell <arg> Shell type. Shell type is auto-detected if option not + present. Valid values: ['bourne', 'cshell']. +The output may include the following variables: + - 'VESPA_TLS_CA_CERT': Path to CA certificates file + - 'VESPA_TLS_CERT': Path to certificate file + - 'VESPA_TLS_PRIVATE_KEY': Path to private key file |