package command

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"os/user"
	"strings"
	"syscall"

	"github.com/hashicorp/errwrap"
	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/builtin/logical/ssh"
	"github.com/mitchellh/cli"
	"github.com/mitchellh/mapstructure"
	"github.com/pkg/errors"
	"github.com/posener/complete"
)

var _ cli.Command = (*SSHCommand)(nil)
var _ cli.CommandAutocomplete = (*SSHCommand)(nil)

type SSHCommand struct {
	*BaseCommand

	// Common SSH options
	flagMode                  string
	flagRole                  string
	flagNoExec                bool
	flagMountPoint            string
	flagStrictHostKeyChecking string
	flagUserKnownHostsFile    string

	// SSH CA Mode options
	flagPublicKeyPath     string
	flagPrivateKeyPath    string
	flagHostKeyMountPoint string
	flagHostKeyHostnames  string
	flagValidPrincipals   string
}

func (c *SSHCommand) Synopsis() string {
	return "Initiate an SSH session"
}

func (c *SSHCommand) Help() string {
	helpText := `
Usage: vault ssh [options] username@ip [ssh options]

  Establishes an SSH connection with the target machine.

  This command uses one of the SSH secrets engines to authenticate and
  automatically establish an SSH connection to a host. This operation requires
  that the SSH secrets engine is mounted and configured.

  SSH using the OTP mode (requires sshpass for full automation):

      $ vault ssh -mode=otp -role=my-role user@1.2.3.4

  SSH using the CA mode:

      $ vault ssh -mode=ca -role=my-role user@1.2.3.4

  SSH using CA mode with host key verification:

      $ vault ssh \
          -mode=ca \
          -role=my-role \
          -host-key-mount-point=host-signer \
          -host-key-hostnames=example.com \
          user@example.com

  For the full list of options and arguments, please see the documentation.

` + c.Flags().Help()

	return strings.TrimSpace(helpText)
}

func (c *SSHCommand) Flags() *FlagSets {
	set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)

	f := set.NewFlagSet("SSH Options")

	// TODO: doc field?

	// General
	f.StringVar(&StringVar{
		Name:       "mode",
		Target:     &c.flagMode,
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictSet("ca", "dynamic", "otp"),
		Usage:      "Name of the authentication mode (ca, dynamic, otp).",
	})

	f.StringVar(&StringVar{
		Name:       "role",
		Target:     &c.flagRole,
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage:      "Name of the role to use to generate the key.",
	})

	f.BoolVar(&BoolVar{
		Name:       "no-exec",
		Target:     &c.flagNoExec,
		Default:    false,
		EnvVar:     "",
		Completion: complete.PredictNothing,
		Usage: "Print the generated credentials, but do not establish a " +
			"connection.",
	})

	f.StringVar(&StringVar{
		Name:       "mount-point",
		Target:     &c.flagMountPoint,
		Default:    "ssh/",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage:      "Mount point to the SSH secrets engine.",
	})

	f.StringVar(&StringVar{
		Name:       "strict-host-key-checking",
		Target:     &c.flagStrictHostKeyChecking,
		Default:    "ask",
		EnvVar:     "VAULT_SSH_STRICT_HOST_KEY_CHECKING",
		Completion: complete.PredictSet("ask", "no", "yes"),
		Usage: "Value to use for the SSH configuration option " +
			"\"StrictHostKeyChecking\".",
	})

	f.StringVar(&StringVar{
		Name:       "user-known-hosts-file",
		Target:     &c.flagUserKnownHostsFile,
		Default:    "~/.ssh/known_hosts",
		EnvVar:     "VAULT_SSH_USER_KNOWN_HOSTS_FILE",
		Completion: complete.PredictFiles("*"),
		Usage: "Value to use for the SSH configuration option " +
			"\"UserKnownHostsFile\".",
	})

	// SSH CA
	f = set.NewFlagSet("CA Mode Options")

	f.StringVar(&StringVar{
		Name:       "public-key-path",
		Target:     &c.flagPublicKeyPath,
		Default:    "~/.ssh/id_rsa.pub",
		EnvVar:     "",
		Completion: complete.PredictFiles("*"),
		Usage:      "Path to the SSH public key to send to Vault for signing.",
	})

	f.StringVar(&StringVar{
		Name:       "private-key-path",
		Target:     &c.flagPrivateKeyPath,
		Default:    "~/.ssh/id_rsa",
		EnvVar:     "",
		Completion: complete.PredictFiles("*"),
		Usage: "Path to the SSH private key to use for authentication. This must " +
			"be the corresponding private key to -public-key-path.",
	})

	f.StringVar(&StringVar{
		Name:       "host-key-mount-point",
		Target:     &c.flagHostKeyMountPoint,
		Default:    "",
		EnvVar:     "VAULT_SSH_HOST_KEY_MOUNT_POINT",
		Completion: complete.PredictAnything,
		Usage: "Mount point to the SSH secrets engine where host keys are signed. " +
			"When given a value, Vault will generate a custom \"known_hosts\" file " +
			"with delegation to the CA at the provided mount point to verify the " +
			"SSH connection's host keys against the provided CA. By default, host " +
			"keys are validated against the user's local \"known_hosts\" file. " +
			"This flag forces strict key host checking and ignores a custom user " +
			"known hosts file.",
	})

	f.StringVar(&StringVar{
		Name:       "host-key-hostnames",
		Target:     &c.flagHostKeyHostnames,
		Default:    "*",
		EnvVar:     "VAULT_SSH_HOST_KEY_HOSTNAMES",
		Completion: complete.PredictAnything,
		Usage: "List of hostnames to delegate for the CA. The default value " +
			"allows all domains and IPs. This is specified as a comma-separated " +
			"list of values.",
	})

	f.StringVar(&StringVar{
		Name:       "valid-principals",
		Target:     &c.flagValidPrincipals,
		Default:    "",
		EnvVar:     "",
		Completion: complete.PredictAnything,
		Usage: "List of valid principal names to include in the generated " +
			"user certificate. This is specified as a comma-separated list of values.",
	})

	return set
}

func (c *SSHCommand) AutocompleteArgs() complete.Predictor {
	return nil
}

func (c *SSHCommand) AutocompleteFlags() complete.Flags {
	return c.Flags().Completions()
}

// Structure to hold the fields returned when asked for a credential from SSH
// secrets engine.
type SSHCredentialResp struct {
	KeyType  string `mapstructure:"key_type"`
	Key      string `mapstructure:"key"`
	Username string `mapstructure:"username"`
	IP       string `mapstructure:"ip"`
	Port     string `mapstructure:"port"`
}

func (c *SSHCommand) Run(args []string) int {
	f := c.Flags()

	if err := f.Parse(args); err != nil {
		c.UI.Error(err.Error())
		return 1
	}

	// Use homedir to expand any relative paths such as ~/.ssh
	c.flagUserKnownHostsFile = expandPath(c.flagUserKnownHostsFile)
	c.flagPublicKeyPath = expandPath(c.flagPublicKeyPath)
	c.flagPrivateKeyPath = expandPath(c.flagPrivateKeyPath)

	args = f.Args()
	if len(args) < 1 {
		c.UI.Error(fmt.Sprintf("Not enough arguments, (expected 1-n, got %d)", len(args)))
		return 1
	}

	// Extract the username and IP.
	username, hostname, ip, err := c.userHostAndIP(args[0])
	if err != nil {
		c.UI.Error(fmt.Sprintf("Error parsing user and IP: %s", err))
		return 1
	}

	// The rest of the args are ssh args
	sshArgs := []string{}
	if len(args) > 1 {
		sshArgs = args[1:]
	}

	// Set the client in the command
	_, err = c.Client()
	if err != nil {
		c.UI.Error(err.Error())
		return 1
	}

	// Credentials are generated only against a registered role. If user
	// does not specify a role with the SSH command, then lookup API is used
	// to fetch all the roles with which this IP is associated. If there is
	// only one role associated with it, use it to establish the connection.
	//
	// TODO: remove in 0.9.0, convert to validation error
	if c.flagRole == "" {
		c.UI.Warn(wrapAtLength(
			"WARNING: No -role specified. Use -role to tell Vault which ssh role " +
				"to use for authentication. In the future, you will need to tell " +
				"Vault which role to use. For now, Vault will attempt to guess based " +
				"on the API response. This will be removed in the Vault 0.11 (or " +
				"later."))

		role, err := c.defaultRole(c.flagMountPoint, ip)
		if err != nil {
			c.UI.Error(fmt.Sprintf("Error choosing role: %v", err))
			return 1
		}
		// Print the default role chosen so that user knows the role name
		// if something doesn't work. If the role chosen is not allowed to
		// be used by the user (ACL enforcement), then user should see an
		// error message accordingly.
		c.UI.Output(fmt.Sprintf("Vault SSH: Role: %q", role))
		c.flagRole = role
	}

	// If no mode was given, perform the old-school lookup. Keep this now for
	// backwards-compatability, but print a warning.
	//
	// TODO: remove in 0.9.0, convert to validation error
	if c.flagMode == "" {
		c.UI.Warn(wrapAtLength(
			"WARNING: No -mode specified. Use -mode to tell Vault which ssh " +
				"authentication mode to use. In the future, you will need to tell " +
				"Vault which mode to use. For now, Vault will attempt to guess based " +
				"on the API response. This guess involves creating a temporary " +
				"credential, reading its type, and then revoking it. To reduce the " +
				"number of API calls and surface area, specify -mode directly. This " +
				"will be removed in Vault 0.11 (or later)."))
		secret, cred, err := c.generateCredential(username, ip)
		if err != nil {
			// This is _very_ hacky, but is the only sane backwards-compatible way
			// to do this. If the error is "key type unknown", we just assume the
			// type is "ca". In the future, mode will be required as an option.
			if strings.Contains(err.Error(), "key type unknown") {
				c.flagMode = ssh.KeyTypeCA
			} else {
				c.UI.Error(fmt.Sprintf("Error getting credential: %s", err))
				return 1
			}
		} else {
			c.flagMode = cred.KeyType
		}

		// Revoke the secret, since the child functions will generate their own
		// credential. Users wishing to avoid this should specify -mode.
		if secret != nil {
			if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
				c.UI.Warn(fmt.Sprintf("Failed to revoke temporary key: %s", err))
			}
		}
	}

	switch strings.ToLower(c.flagMode) {
	case ssh.KeyTypeCA:
		return c.handleTypeCA(username, hostname, ip, sshArgs)
	case ssh.KeyTypeOTP:
		return c.handleTypeOTP(username, ip, sshArgs)
	case ssh.KeyTypeDynamic:
		return c.handleTypeDynamic(username, ip, sshArgs)
	default:
		c.UI.Error(fmt.Sprintf("Unknown SSH mode: %s", c.flagMode))
		return 1
	}
}

// handleTypeCA is used to handle SSH logins using the "CA" key type.
func (c *SSHCommand) handleTypeCA(username, hostname, ip string, sshArgs []string) int {
	// Read the key from disk
	publicKey, err := ioutil.ReadFile(c.flagPublicKeyPath)
	if err != nil {
		c.UI.Error(fmt.Sprintf("failed to read public key %s: %s",
			c.flagPublicKeyPath, err))
		return 1
	}

	sshClient := c.client.SSHWithMountPoint(c.flagMountPoint)

	var principals = username
	if c.flagValidPrincipals != "" {
		principals = c.flagValidPrincipals
	}

	// Attempt to sign the public key
	secret, err := sshClient.SignKey(c.flagRole, map[string]interface{}{
		// WARNING: publicKey is []byte, which is b64 encoded on JSON upload. We
		// have to convert it to a string. SV lost many hours to this...
		"public_key":       string(publicKey),
		"valid_principals": principals,
		"cert_type":        "user",

		// TODO: let the user configure these. In the interim, if users want to
		// customize these values, they can produce the key themselves.
		"extensions": map[string]string{
			"permit-X11-forwarding":   "",
			"permit-agent-forwarding": "",
			"permit-port-forwarding":  "",
			"permit-pty":              "",
			"permit-user-rc":          "",
		},
	})
	if err != nil {
		c.UI.Error(fmt.Sprintf("failed to sign public key %s: %s",
			c.flagPublicKeyPath, err))
		return 2
	}
	if secret == nil || secret.Data == nil {
		c.UI.Error("missing signed key")
		return 2
	}

	// Handle no-exec
	if c.flagNoExec {
		if c.flagField != "" {
			return PrintRawField(c.UI, secret, c.flagField)
		}
		return OutputSecret(c.UI, secret)
	}

	// Extract public key
	key, ok := secret.Data["signed_key"].(string)
	if !ok || key == "" {
		c.UI.Error("signed key is empty")
		return 2
	}

	// Capture the current value - this could be overwritten later if the user
	// enabled host key signing verification.
	userKnownHostsFile := c.flagUserKnownHostsFile
	strictHostKeyChecking := c.flagStrictHostKeyChecking

	// Handle host key signing verification. If the user specified a mount point,
	// download the public key, trust it with the given domains, and use that
	// instead of the user's regular known_hosts file.
	if c.flagHostKeyMountPoint != "" {
		secret, err := c.client.Logical().Read(c.flagHostKeyMountPoint + "/config/ca")
		if err != nil {
			c.UI.Error(fmt.Sprintf("failed to get host signing key: %s", err))
			return 2
		}
		if secret == nil || secret.Data == nil {
			c.UI.Error("missing host signing key")
			return 2
		}
		publicKey, ok := secret.Data["public_key"].(string)
		if !ok || publicKey == "" {
			c.UI.Error("host signing key is empty")
			return 2
		}

		// Write the known_hosts file
		name := fmt.Sprintf("vault_ssh_ca_known_hosts_%s_%s", username, ip)
		data := fmt.Sprintf("@cert-authority %s %s", c.flagHostKeyHostnames, publicKey)
		knownHosts, err, closer := c.writeTemporaryFile(name, []byte(data), 0644)
		defer closer()
		if err != nil {
			c.UI.Error(fmt.Sprintf("failed to write host public key: %s", err))
			return 1
		}

		// Update the variables
		userKnownHostsFile = knownHosts
		strictHostKeyChecking = "yes"
	}

	// Write the signed public key to disk
	name := fmt.Sprintf("vault_ssh_ca_%s_%s", username, ip)
	signedPublicKeyPath, err, closer := c.writeTemporaryKey(name, []byte(key))
	defer closer()
	if err != nil {
		c.UI.Error(fmt.Sprintf("failed to write signed public key: %s", err))
		return 2
	}

	args := append([]string{
		"-i", c.flagPrivateKeyPath,
		"-i", signedPublicKeyPath,
		"-o UserKnownHostsFile=" + userKnownHostsFile,
		"-o StrictHostKeyChecking=" + strictHostKeyChecking,
		username + "@" + hostname,
	}, sshArgs...)

	cmd := exec.Command("ssh", args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		exitCode := 2

		if exitError, ok := err.(*exec.ExitError); ok {
			if exitError.Success() {
				return 0
			}
			if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
				exitCode = ws.ExitStatus()
			}
		}

		c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err))
		return exitCode
	}

	// There is no secret to revoke, since it's a certificate signing
	return 0
}

// handleTypeOTP is used to handle SSH logins using the "otp" key type.
func (c *SSHCommand) handleTypeOTP(username, ip string, sshArgs []string) int {
	secret, cred, err := c.generateCredential(username, ip)
	if err != nil {
		c.UI.Error(fmt.Sprintf("failed to generate credential: %s", err))
		return 2
	}

	// Handle no-exec
	if c.flagNoExec {
		if c.flagField != "" {
			return PrintRawField(c.UI, secret, c.flagField)
		}
		return OutputSecret(c.UI, secret)
	}

	var cmd *exec.Cmd

	// Check if the application 'sshpass' is installed in the client machine. If
	// it is then, use it to automate typing in OTP to the prompt. Unfortunately,
	// it was not possible to automate it without a third-party application, with
	// only the Go libraries. Feel free to try and remove this dependency.
	sshpassPath, err := exec.LookPath("sshpass")
	if err != nil {
		c.UI.Warn(wrapAtLength(
			"Vault could not locate \"sshpass\". The OTP code for the session is " +
				"displayed below. Enter this code in the SSH password prompt. If you " +
				"install sshpass, Vault can automatically perform this step for you."))
		c.UI.Output("OTP for the session is: " + cred.Key)

		args := append([]string{
			"-o UserKnownHostsFile=" + c.flagUserKnownHostsFile,
			"-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking,
			"-p", cred.Port,
			username + "@" + ip,
		}, sshArgs...)
		cmd = exec.Command("ssh", args...)
	} else {
		args := append([]string{
			"-e", // Read password for SSHPASS environment variable
			"ssh",
			"-o UserKnownHostsFile=" + c.flagUserKnownHostsFile,
			"-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking,
			"-p", cred.Port,
			username + "@" + ip,
		}, sshArgs...)
		cmd = exec.Command(sshpassPath, args...)
		env := os.Environ()
		env = append(env, fmt.Sprintf("SSHPASS=%s", string(cred.Key)))
		cmd.Env = env
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		exitCode := 2

		if exitError, ok := err.(*exec.ExitError); ok {
			if exitError.Success() {
				return 0
			}
			if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
				exitCode = ws.ExitStatus()
			}
		}

		c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err))
		return exitCode
	}

	// Revoke the key if it's longer than expected
	if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
		c.UI.Error(fmt.Sprintf("failed to revoke key: %s", err))
		return 2
	}

	return 0
}

// handleTypeDynamic is used to handle SSH logins using the "dyanmic" key type.
func (c *SSHCommand) handleTypeDynamic(username, ip string, sshArgs []string) int {
	// Generate the credential
	secret, cred, err := c.generateCredential(username, ip)
	if err != nil {
		c.UI.Error(fmt.Sprintf("failed to generate credential: %s", err))
		return 2
	}

	// Handle no-exec
	if c.flagNoExec {
		if c.flagField != "" {
			return PrintRawField(c.UI, secret, c.flagField)
		}
		return OutputSecret(c.UI, secret)
	}

	// Write the dynamic key to disk
	name := fmt.Sprintf("vault_ssh_dynamic_%s_%s", username, ip)
	keyPath, err, closer := c.writeTemporaryKey(name, []byte(cred.Key))
	defer closer()
	if err != nil {
		c.UI.Error(fmt.Sprintf("failed to write dynamic key: %s", err))
		return 1
	}

	args := append([]string{
		"-i", keyPath,
		"-o UserKnownHostsFile=" + c.flagUserKnownHostsFile,
		"-o StrictHostKeyChecking=" + c.flagStrictHostKeyChecking,
		"-p", cred.Port,
		username + "@" + ip,
	}, sshArgs...)

	cmd := exec.Command("ssh", args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	err = cmd.Run()
	if err != nil {
		exitCode := 2

		if exitError, ok := err.(*exec.ExitError); ok {
			if exitError.Success() {
				return 0
			}
			if ws, ok := exitError.Sys().(syscall.WaitStatus); ok {
				exitCode = ws.ExitStatus()
			}
		}

		c.UI.Error(fmt.Sprintf("failed to run ssh command: %s", err))
		return exitCode
	}

	// Revoke the key if it's longer than expected
	if err := c.client.Sys().Revoke(secret.LeaseID); err != nil {
		c.UI.Error(fmt.Sprintf("failed to revoke key: %s", err))
		return 2
	}

	return 0
}

// generateCredential generates a credential for the given role and returns the
// decoded secret data.
func (c *SSHCommand) generateCredential(username, ip string) (*api.Secret, *SSHCredentialResp, error) {
	sshClient := c.client.SSHWithMountPoint(c.flagMountPoint)

	// Attempt to generate the credential.
	secret, err := sshClient.Credential(c.flagRole, map[string]interface{}{
		"username": username,
		"ip":       ip,
	})
	if err != nil {
		return nil, nil, errors.Wrap(err, "failed to get credentials")
	}
	if secret == nil || secret.Data == nil {
		return nil, nil, fmt.Errorf("vault returned empty credentials")
	}

	// Port comes back as a json.Number which mapstructure doesn't like, so
	// convert it
	if d, ok := secret.Data["port"].(json.Number); ok {
		secret.Data["port"] = d.String()
	}

	// Use mapstructure to decode the response
	var resp SSHCredentialResp
	if err := mapstructure.Decode(secret.Data, &resp); err != nil {
		return nil, nil, errors.Wrap(err, "failed to decode credential")
	}

	// Check for an empty key response
	if len(resp.Key) == 0 {
		return nil, nil, fmt.Errorf("vault returned an invalid key")
	}

	return secret, &resp, nil
}

// writeTemporaryFile writes a file to a temp location with the given data and
// file permissions.
func (c *SSHCommand) writeTemporaryFile(name string, data []byte, perms os.FileMode) (string, error, func() error) {
	// default closer to prevent panic
	closer := func() error { return nil }

	f, err := ioutil.TempFile("", name)
	if err != nil {
		return "", errors.Wrap(err, "creating temporary file"), closer
	}

	closer = func() error { return os.Remove(f.Name()) }

	if err := ioutil.WriteFile(f.Name(), data, perms); err != nil {
		return "", errors.Wrap(err, "writing temporary key"), closer
	}

	return f.Name(), nil, closer
}

// writeTemporaryKey writes the key to a temporary file and returns the path.
// The caller should defer the closer to cleanup the key.
func (c *SSHCommand) writeTemporaryKey(name string, data []byte) (string, error, func() error) {
	return c.writeTemporaryFile(name, data, 0600)
}

// If user did not provide the role with which SSH connection has
// to be established and if there is only one role associated with
// the IP, it is used by default.
func (c *SSHCommand) defaultRole(mountPoint, ip string) (string, error) {
	data := map[string]interface{}{
		"ip": ip,
	}
	secret, err := c.client.Logical().Write(mountPoint+"/lookup", data)
	if err != nil {
		return "", errwrap.Wrapf(fmt.Sprintf("error finding roles for IP %q: {{err}}", ip), err)
	}
	if secret == nil || secret.Data == nil {
		return "", errwrap.Wrapf(fmt.Sprintf("error finding roles for IP %q: {{err}}", ip), err)
	}

	if secret.Data["roles"] == nil {
		return "", fmt.Errorf("no matching roles found for IP %q", ip)
	}

	if len(secret.Data["roles"].([]interface{})) == 1 {
		return secret.Data["roles"].([]interface{})[0].(string), nil
	} else {
		var roleNames string
		for _, item := range secret.Data["roles"].([]interface{}) {
			roleNames += item.(string) + ", "
		}
		roleNames = strings.TrimRight(roleNames, ", ")
		return "", fmt.Errorf("Roles: %q. "+`
			Multiple roles are registered for this IP.
			Select a role using '-role' option.
			Note that all roles may not be permitted, based on ACLs.`, roleNames)
	}
}

// userAndIP takes an argument in the format foo@1.2.3.4 and separates the IP
// and user parts, returning any errors.
func (c *SSHCommand) userHostAndIP(s string) (string, string, string, error) {
	// split the parameter username@ip
	input := strings.Split(s, "@")
	var username, address string

	// If only IP is mentioned and username is skipped, assume username to
	// be the current username. Vault SSH role's default username could have
	// been used, but in order to retain the consistency with SSH command,
	// current username is employed.
	switch len(input) {
	case 1:
		u, err := user.Current()
		if err != nil {
			return "", "", "", errors.Wrap(err, "failed to fetch current user")
		}
		username, address = u.Username, input[0]
	case 2:
		username, address = input[0], input[1]
	default:
		return "", "", "", fmt.Errorf("invalid arguments: %q", s)
	}

	// Resolving domain names to IP address on the client side.
	// Vault only deals with IP addresses.
	ipAddr, err := net.ResolveIPAddr("ip", address)
	if err != nil {
		return "", "", "", errors.Wrap(err, "failed to resolve IP address")
	}
	ip := ipAddr.String()

	return username, address, ip, nil
}
