Skip to content
12 changes: 7 additions & 5 deletions cmd/topo/setup_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ var setupKeysCmd = &cobra.Command{
}

dest := ssh.NewDestination(targetArg)
user, err := ssh.GetUserFromConfig(dest)
if err != nil {
return fmt.Errorf("%w; note: a per user SSH config entry should be created when setting up keys", err)
}

dest.User = user
targetSlug := dest.Slugify()
if privateKeyPath == "" {
privateKeyPath, err = setupkeys.GetDefaultPrivateKeyPath(targetSlug)
Expand All @@ -46,11 +52,6 @@ var setupKeysCmd = &cobra.Command{
return err
}

err = ssh.IsDestinationAlreadyConfiguredWithAnotherUser(dest)
if err != nil {
return fmt.Errorf("%w; note: a per user SSH config entry should be created when setting up keys", err)
}

seq, err := setupkeys.NewKeySetup(dest, privateKeyPath, parsedKeyType)
if err != nil {
return err
Expand All @@ -64,6 +65,7 @@ var setupKeysCmd = &cobra.Command{
modifiers := []ssh.ConfigDirectiveModifier{
ssh.NewEnsureConfigDirectivePath("IdentityFile", privateKeyPath),
ssh.NewEnsureConfigDirective("IdentitiesOnly", "yes"),
ssh.NewEnsureConfigDirective("User", dest.User),
}

return ssh.CreateOrModifyConfigFile(dest, modifiers)
Expand Down
40 changes: 22 additions & 18 deletions internal/ssh/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,35 @@ func NewConfigFromBytes(data []byte) Config {
return config
}

func IsDestinationAlreadyConfiguredWithAnotherUser(dest Destination) error {
hostConfig, err := LookupExplicitHostConfig(dest.Host, dest.Port)
func GetUserFromConfig(dest Destination) (string, error) {
output, err := readConfig(Destination{Host: dest.Host, Port: dest.Port})
if err != nil {
return err
return "", err
}

if dest.User != "" && hostConfig.User != dest.User {
return fmt.Errorf("ssh host/alias %q is already associated with user %q", dest.Host, hostConfig.User)
}
return nil
return ResolveConfiguredUser(dest, output)
}

// LookupExplicitHostConfig returns configuration, only if there's an explicit entry for the given host/port
func LookupExplicitHostConfig(host, port string) (Config, error) {
dest := Destination{Host: host, Port: port}
output, err := readConfig(dest)
if err != nil {
return Config{}, err
}
func ResolveConfiguredUser(dest Destination, configOutput []byte) (string, error) {
hostConfig := NewConfigFromBytes(configOutput)

if !IsExplicitHostConfig(host, output) {
return Config{}, fmt.Errorf("no explicit host config found for %s", host)
if IsExplicitHostConfig(dest.Host, configOutput) {
if hostConfig.User != "" && dest.User != "" && hostConfig.User != dest.User {
return "", fmt.Errorf(
"ssh host/alias %q is already associated with user %q",
dest.Host,
hostConfig.User,
)
}
if dest.User != "" {
return dest.User, nil
}
return hostConfig.User, nil
}

return NewConfigFromBytes(output), nil
if dest.User != "" {
return dest.User, nil
}
return hostConfig.User, nil
}

func IsExplicitHostConfig(host string, config []byte) bool {
Expand Down
74 changes: 74 additions & 0 deletions internal/ssh/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,80 @@ user homer
})
}

func TestResolveConfiguredUser(t *testing.T) {
t.Run("with explicit configuration", func(t *testing.T) {
explicitConfig := []byte(`debug1: /tmp/.ssh/topo_config line 1: Applying options for board-alias
debug1: /etc/ssh/ssh_config line 57: Applying options for *
user root
hostname 10.2.2.26
`)

t.Run("returns destination's user when no user is configured", func(t *testing.T) {
config := []byte(`debug1: /tmp/.ssh/topo_config line 1: Applying options for board-alias
debug1: /etc/ssh/ssh_config line 57: Applying options for *
hostname 10.2.2.26
`)
dest := ssh.Destination{User: "root", Host: "board-alias"}

got, err := ssh.ResolveConfiguredUser(dest, config)

assert.NoError(t, err)
assert.Equal(t, "root", got)
})

t.Run("returns destination's user when user is configured as the same user", func(t *testing.T) {
dest := ssh.Destination{User: "root", Host: "board-alias"}

got, err := ssh.ResolveConfiguredUser(dest, explicitConfig)

assert.NoError(t, err)
assert.Equal(t, "root", got)
})

t.Run("returns configured user when destination's user is not set", func(t *testing.T) {
dest := ssh.Destination{Host: "board-alias"}

got, err := ssh.ResolveConfiguredUser(dest, explicitConfig)

assert.NoError(t, err)
assert.Equal(t, "root", got)
})

t.Run("errors if destination's user doesn't match configured user", func(t *testing.T) {
dest := ssh.Destination{User: "admin", Host: "board-alias"}

_, err := ssh.ResolveConfiguredUser(dest, explicitConfig)

assert.ErrorContains(t, err, `ssh host/alias "board-alias" is already associated with user "root"`)
})
})

t.Run("without explicit host configuration", func(t *testing.T) {
wildcardConfig := []byte(`debug1: /etc/ssh/ssh_config line 57: Applying options for *
user username
hostname board-alias
`)

t.Run("returns destination's user when it's set", func(t *testing.T) {
dest := ssh.Destination{User: "root", Host: "board-alias"}

got, err := ssh.ResolveConfiguredUser(dest, wildcardConfig)

assert.NoError(t, err)
assert.Equal(t, "root", got)
})

t.Run("returns configured user when destination's user is not set", func(t *testing.T) {
dest := ssh.Destination{Host: "board-alias"}

got, err := ssh.ResolveConfiguredUser(dest, wildcardConfig)

assert.NoError(t, err)
assert.Equal(t, "username", got)
})
})
}

func TestIsExplicitHostConfig(t *testing.T) {
t.Run("returns true for exact host matches in verbose ssh output", func(t *testing.T) {
config := []byte(`debug1: /tmp/config line 1: Applying options for Board,board-alt
Expand Down
Loading