Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ _tmp/
.cache
*.json
*.out
*.html
*.log
37 changes: 21 additions & 16 deletions cmd/oneauth/commands/cmd_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,7 @@
Usage: "SSH Agent",
Description: "All configuration options can be set in the config file",
Before: func(_ *cli.Context) error {
version := buildinfo.Version

commit := buildinfo.Commit
if len(commit) > 8 {
version += "-" + commit[:8]
}

log.Printf("OneAuth version: %s", version)
log.Printf("OneAuth version: %s", buildinfo.FormattedVersion())

Check warning on line 27 in cmd/oneauth/commands/cmd_agent.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_agent.go#L27

Added line #L27 was not covered by tests

return nil
},
Expand All @@ -45,14 +38,26 @@

log := logger.New(config.AgentLogPath)

if config.Keyring.Yubikey.Serial == 0 {
return fmt.Errorf("yubikey serial is required")
}

var agent *sshagent.SSHAgent

switch config.Socket.Type {
case "unix":
// Determine YubiKey serial to use
var yubikeySerial uint32
if config.Keyring.DisableYubikey {
log.Println("YubiKey keyring disabled - running without hardware authentication")
yubikeySerial = 0 // Disabled mode

} else {
// YubiKey serial is required for unix socket type when not disabled
if config.Keyring.Yubikey.Serial == 0 {
return fmt.Errorf("yubikey serial is required for unix socket type (or set disable_yubikey: true)")
}
yubikeySerial = config.Keyring.Yubikey.Serial
log.WithField("yubikey", yubikeySerial).Println("opening yubikey:", yubikeySerial)

Check warning on line 57 in cmd/oneauth/commands/cmd_agent.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_agent.go#L45-L57

Added lines #L45 - L57 were not covered by tests
}

// Set up socket
if _, err := os.Stat(config.Socket.Path); err == nil {
os.Remove(config.Socket.Path)
}
Expand All @@ -61,9 +66,8 @@
return fmt.Errorf("failed to create directory: %w", err)
}

log.WithField("yubikey", config.Keyring.Yubikey.Serial).Println("opening yubikey:", config.Keyring.Yubikey.Serial)

agent, err = sshagent.New(config.Keyring.Yubikey.Serial, log, config)
// Create agent
agent, err = sshagent.New(yubikeySerial, log, config)

Check warning on line 70 in cmd/oneauth/commands/cmd_agent.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_agent.go#L70

Added line #L70 was not covered by tests
if err != nil {
return fmt.Errorf("failed to create agent: %w", err)
}
Expand All @@ -73,7 +77,8 @@
})

case "dummy":
log.Println("skipping socket creation")
log.Println("skipping socket creation - running in dummy mode")

Check warning on line 80 in cmd/oneauth/commands/cmd_agent.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_agent.go#L80

Added line #L80 was not covered by tests
// For dummy mode, we don't need YubiKey, so agent remains nil

default:
return fmt.Errorf("socket type %s is not supported", config.Socket.Type)
Expand Down
204 changes: 204 additions & 0 deletions cmd/oneauth/commands/cmd_agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package commands

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/vitalvas/oneauth/cmd/oneauth/config"
)

func TestAgentValidation_YubikeySerialRequirement(t *testing.T) {
t.Run("UnixSocketRequiresYubikeySerial", func(t *testing.T) {
// Create a temporary config file with serial = 0
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

configContent := `
socket:
type: unix
path: ` + filepath.Join(tempDir, "test.sock") + `
control_socket_path: ` + filepath.Join(tempDir, "test_ctrl.sock") + `
agent_log_path: ` + filepath.Join(tempDir, "test.log") + `
keyring:
yubikey:
serial: 0
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)

// Load the config
cfg, err := config.Load(configPath)
assert.NoError(t, err)

// Test the validation logic that's in the agent command
if cfg.Socket.Type == "unix" && !cfg.Keyring.DisableYubikey && cfg.Keyring.Yubikey.Serial == 0 {
err = assert.AnError // Simulate the error that would be returned
}

// Should detect that YubiKey serial is required
assert.Error(t, err, "Expected error when YubiKey serial is 0 for unix socket")
})

t.Run("DummySocketDoesNotRequireYubikeySerial", func(t *testing.T) {
// Create a temporary config file with dummy socket and serial = 0
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

configContent := `
socket:
type: dummy
control_socket_path: ` + filepath.Join(tempDir, "test_ctrl.sock") + `
agent_log_path: ` + filepath.Join(tempDir, "test.log") + `
keyring:
yubikey:
serial: 0
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)

// Load the config
cfg, err := config.Load(configPath)
assert.NoError(t, err)

// Test the validation logic - dummy socket should not require YubiKey
var validationErr error
if cfg.Socket.Type == "unix" && !cfg.Keyring.DisableYubikey && cfg.Keyring.Yubikey.Serial == 0 {
validationErr = assert.AnError // This should NOT trigger for dummy
}

// Should NOT require YubiKey serial for dummy socket
assert.NoError(t, validationErr, "Dummy socket should not require YubiKey serial")
assert.Equal(t, "dummy", cfg.Socket.Type)
assert.Equal(t, uint32(0), cfg.Keyring.Yubikey.Serial)
})

t.Run("UnixSocketWithValidSerial", func(t *testing.T) {
// Create a temporary config file with valid serial
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

configContent := `
socket:
type: unix
path: ` + filepath.Join(tempDir, "test.sock") + `
control_socket_path: ` + filepath.Join(tempDir, "test_ctrl.sock") + `
agent_log_path: ` + filepath.Join(tempDir, "test.log") + `
keyring:
yubikey:
serial: 12345
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)

// Load the config
cfg, err := config.Load(configPath)
assert.NoError(t, err)

// Test the validation logic
var validationErr error
if cfg.Socket.Type == "unix" && !cfg.Keyring.DisableYubikey && cfg.Keyring.Yubikey.Serial == 0 {
validationErr = assert.AnError // This should NOT trigger with valid serial
}

// Should pass validation with valid serial
assert.NoError(t, validationErr)
assert.Equal(t, "unix", cfg.Socket.Type)
assert.Equal(t, uint32(12345), cfg.Keyring.Yubikey.Serial)
})

t.Run("UnixSocketWithDisabledYubikey", func(t *testing.T) {
// Create a temporary config file with disabled YubiKey
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

configContent := `
socket:
type: unix
path: ` + filepath.Join(tempDir, "test.sock") + `
control_socket_path: ` + filepath.Join(tempDir, "test_ctrl.sock") + `
agent_log_path: ` + filepath.Join(tempDir, "test.log") + `
keyring:
disable_yubikey: true
yubikey:
serial: 0
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)

// Load the config
cfg, err := config.Load(configPath)
assert.NoError(t, err)

// Test the validation logic - should not require serial when disabled
var validationErr error
if cfg.Socket.Type == "unix" && !cfg.Keyring.DisableYubikey && cfg.Keyring.Yubikey.Serial == 0 {
validationErr = assert.AnError // This should NOT trigger when disabled
}

// Should pass validation when YubiKey is disabled
assert.NoError(t, validationErr)
assert.Equal(t, "unix", cfg.Socket.Type)
assert.True(t, cfg.Keyring.DisableYubikey)
assert.Equal(t, uint32(0), cfg.Keyring.Yubikey.Serial)
})

t.Run("UnixSocketWithDisabledYubikeyButValidSerial", func(t *testing.T) {
// Create a temporary config file with disabled YubiKey but valid serial
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

configContent := `
socket:
type: unix
path: ` + filepath.Join(tempDir, "test.sock") + `
control_socket_path: ` + filepath.Join(tempDir, "test_ctrl.sock") + `
agent_log_path: ` + filepath.Join(tempDir, "test.log") + `
keyring:
disable_yubikey: true
yubikey:
serial: 12345
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)

// Load the config
cfg, err := config.Load(configPath)
assert.NoError(t, err)

// Should pass validation - when disabled, serial is ignored
assert.Equal(t, "unix", cfg.Socket.Type)
assert.True(t, cfg.Keyring.DisableYubikey)
assert.Equal(t, uint32(12345), cfg.Keyring.Yubikey.Serial) // Serial is present but ignored
})
}

func TestAgentValidation_SocketTypes(t *testing.T) {
t.Run("ValidSocketTypes", func(t *testing.T) {
validTypes := []string{"unix", "dummy"}

for _, socketType := range validTypes {
tempDir := t.TempDir()
configPath := filepath.Join(tempDir, "config.yaml")

configContent := `
socket:
type: ` + socketType + `
path: ` + filepath.Join(tempDir, "test.sock") + `
control_socket_path: ` + filepath.Join(tempDir, "test_ctrl.sock") + `
agent_log_path: ` + filepath.Join(tempDir, "test.log") + `
keyring:
yubikey:
serial: 12345
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
assert.NoError(t, err)

// Load the config
cfg, err := config.Load(configPath)
assert.NoError(t, err)
assert.Equal(t, socketType, cfg.Socket.Type)
}
})
}
71 changes: 64 additions & 7 deletions cmd/oneauth/commands/cmd_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"strings"

"github.com/urfave/cli/v2"
"github.com/vitalvas/oneauth/cmd/oneauth/config"
"github.com/vitalvas/oneauth/cmd/oneauth/rpclient"
"github.com/vitalvas/oneauth/internal/yubikey"
)

Expand All @@ -15,6 +17,13 @@
{{- range $key := .Keys }}
- {{ $key.Name }} (Serial: {{ $key.Serial }}, Version: {{ $key.Version }})
{{- end }}
{{- if .AgentPid }}

--- Agent ---
Agent PID: {{ .AgentPid }}
Version: {{ .AgentVersion }}
Uptime: {{ .AgentUptime }}
{{- end }}
`

type InfoKey struct {
Expand All @@ -24,20 +33,71 @@
}

type infoData struct {
Keys []InfoKey
Keys []InfoKey
AgentPid int
AgentVersion string
AgentUptime string
}

var infoCmd = &cli.Command{
Name: "info",
Usage: "Prints detailed information",
Action: func(_ *cli.Context) error {
Before: loadConfig,
Action: func(c *cli.Context) error {
info := infoData{}

// Try to get info from running agent via control socket first
if globalConfig != nil {
client, err := rpclient.New(globalConfig.ControlSocketPath)
if err == nil {
defer client.Close()

rpcInfo, err := client.GetInfo()
if err == nil {
info.AgentPid = rpcInfo.Pid
info.AgentVersion = rpcInfo.Version
info.AgentUptime = rpcInfo.Uptime
// Use keys from the running agent
for _, key := range rpcInfo.Keys {
info.Keys = append(info.Keys, InfoKey{
Name: key.Name,
Serial: key.Serial,
Version: key.Version,
})
}

Check warning on line 67 in cmd/oneauth/commands/cmd_info.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_info.go#L46-L67

Added lines #L46 - L67 were not covered by tests

render := func(tmpl string, data interface{}) string {
var out strings.Builder
if err := template.Must(template.New("tmpl").Parse(tmpl)).Execute(&out, data); err != nil {
log.Fatalf("failed to render template: %v", err)
}
return strings.TrimSpace(out.String())

Check warning on line 74 in cmd/oneauth/commands/cmd_info.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_info.go#L69-L74

Added lines #L69 - L74 were not covered by tests
}

fmt.Println(render(infoTmpl, info))
return nil

Check warning on line 78 in cmd/oneauth/commands/cmd_info.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_info.go#L77-L78

Added lines #L77 - L78 were not covered by tests
}
}
}

// Fallback to local YubiKey detection when control socket is unavailable
var configPath string
if globalConfig == nil {
configPath = c.String("config")
cfg, err := config.Load(configPath)
if err != nil {
// Continue with YubiKey detection even if config fails
log.Printf("Warning: failed to load config: %v", err)
} else {
globalConfig = cfg
}

Check warning on line 93 in cmd/oneauth/commands/cmd_info.go

View check run for this annotation

Codecov / codecov/patch

cmd/oneauth/commands/cmd_info.go#L84-L93

Added lines #L84 - L93 were not covered by tests
}

cards, err := yubikey.Cards()
if err != nil {
return err
}

info := infoData{}

for _, card := range cards {
info.Keys = append(info.Keys, InfoKey{
Name: card.String(),
Expand All @@ -48,16 +108,13 @@

render := func(tmpl string, data interface{}) string {
var out strings.Builder

if err := template.Must(template.New("tmpl").Parse(tmpl)).Execute(&out, data); err != nil {
log.Fatalf("failed to render template: %v", err)
}

return strings.TrimSpace(out.String())
}

fmt.Println(render(infoTmpl, info))

return nil
},
}
Loading
Loading