Skip to content

Commit 5d68f9b

Browse files
committed
feat: Add username field to platform config for SSH auth enforcement
- Add Username field to transcript.Platform and fakedevices.FakeDevice - InitGeneric populates Username from platform config - PasswordHandler enforces username when set (any username accepted if empty) - Update Junos platform entry with username: admin - Add TestGenericListener_UsernameEnforcement - Document username field in configuration.md and transcripts.md Closes #89
1 parent 2f35a9b commit 5d68f9b

7 files changed

Lines changed: 75 additions & 1 deletion

File tree

docs/configuration.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ The device vendor (e.g., `cisco`, `arista`, `juniper`).
4444

4545
The hostname displayed in the CLI prompt. Supports Go template variables (see [Transcripts](transcripts.md)).
4646

47+
#### username
48+
49+
Optional. The SSH username required to authenticate. When set, cisshgo enforces both username and password — connections with a different username are rejected. When omitted, any username is accepted (only the password is checked).
50+
51+
```yaml
52+
junos:
53+
username: "admin" # only "admin" can connect
54+
password: "admin"
55+
```
56+
57+
Also available as `{{.Username}}` in transcript templates.
58+
4759
#### password
4860

4961
SSH password for authentication.

docs/transcripts.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type FakeDevice struct {
4646
Vendor string // Device vendor (e.g., "cisco")
4747
Platform string // Platform identifier (e.g., "csr1000v")
4848
Hostname string // Device hostname
49+
Username string // SSH username (from platform config)
4950
Password string // SSH password
5051
SupportedCommands SupportedCommands // Available commands
5152
ContextSearch map[string]string // CLI contexts
@@ -241,7 +242,7 @@ The file should be empty or contain only whitespace.
241242
If you see errors like `template: ...: executing ... at <.InvalidField>: can't evaluate field`:
242243

243244
- Check that the field name matches exactly (case-sensitive)
244-
- Available fields: `Vendor`, `Platform`, `Hostname`, `Password`
245+
- Available fields: `Vendor`, `Platform`, `Hostname`, `Username`, `Password`
245246
- Use `{{.Hostname}}` not `{{.hostname}}`
246247

247248
### Command Not Found

fakedevices/genericFakeDevice.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type FakeDevice struct {
1919
Platform string // Platform of this fake device
2020
Hostname string // Hostname of the fake device
2121
DefaultHostname string // Default Hostname of the fake device (for resetting)
22+
Username string // Expected SSH username (empty = any username accepted)
2223
Password string // Password of the fake device
2324
SupportedCommands SupportedCommands // What commands this fake device supports
2425
ContextSearch map[string]string // The available CLI prompt/contexts on this fake device
@@ -102,6 +103,7 @@ func InitGeneric(platform string, myTranscriptMap transcript.Map, baseDir string
102103
Platform: platform,
103104
Hostname: p.Hostname,
104105
DefaultHostname: p.Hostname,
106+
Username: p.Username,
105107
Password: p.Password,
106108
SupportedCommands: supportedCommands,
107109
ContextSearch: p.ContextSearch,

ssh_server/sshlisteners/sshlisteners.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ func listen(ctx context.Context, myFakeDevice *fakedevices.FakeDevice, portNumbe
4242
Addr: portString,
4343
Handler: handler,
4444
PasswordHandler: func(sshCtx ssh.Context, pass string) bool {
45+
if myFakeDevice.Username != "" && sshCtx.User() != myFakeDevice.Username {
46+
return false
47+
}
4548
return pass == myFakeDevice.Password
4649
},
4750
}

ssh_server/sshlisteners/sshlisteners_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,57 @@ func TestGenericListener_PortInUse(t *testing.T) {
104104
t.Error("expected error when port is already in use")
105105
}
106106
}
107+
108+
func TestGenericListener_UsernameEnforcement(t *testing.T) {
109+
fd := &fakedevices.FakeDevice{
110+
Hostname: "testhost",
111+
DefaultHostname: "testhost",
112+
Username: "admin",
113+
Password: "admin",
114+
SupportedCommands: fakedevices.SupportedCommands{},
115+
ContextSearch: map[string]string{"base": ">"},
116+
ContextHierarchy: map[string]string{">": "exit"},
117+
}
118+
119+
ln, err := net.Listen("tcp", "127.0.0.1:0")
120+
if err != nil {
121+
t.Fatal(err)
122+
}
123+
port := ln.Addr().(*net.TCPAddr).Port
124+
addr := ln.Addr().String()
125+
ln.Close()
126+
127+
ctx, cancel := context.WithCancel(context.Background())
128+
defer cancel()
129+
go func() { GenericListener(ctx, fd, port, handlers.GenericCiscoHandler) }()
130+
131+
for i := 0; i < 20; i++ {
132+
conn, dialErr := net.DialTimeout("tcp", addr, 100*time.Millisecond)
133+
if dialErr == nil {
134+
conn.Close()
135+
break
136+
}
137+
time.Sleep(50 * time.Millisecond)
138+
}
139+
140+
cfg := &gossh.ClientConfig{
141+
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
142+
Timeout: 2 * time.Second,
143+
}
144+
145+
// Correct username + password — should succeed
146+
cfg.User = "admin"
147+
cfg.Auth = []gossh.AuthMethod{gossh.Password("admin")}
148+
client, err := gossh.Dial("tcp", addr, cfg)
149+
if err != nil {
150+
t.Fatalf("expected success with correct credentials: %v", err)
151+
}
152+
client.Close()
153+
154+
// Wrong username — should fail
155+
cfg.User = "wronguser"
156+
_, err = gossh.Dial("tcp", addr, cfg)
157+
if err == nil {
158+
t.Error("expected auth failure with wrong username")
159+
}
160+
}

transcript/transcript.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Scenario struct {
2727
type Platform struct {
2828
Vendor string `yaml:"vendor" json:"vendor"`
2929
Hostname string `yaml:"hostname" json:"hostname"`
30+
Username string `yaml:"username" json:"username"`
3031
Password string `yaml:"password" json:"password"`
3132
CommandTranscripts map[string]string `yaml:"command_transcripts" json:"command_transcripts"`
3233
ContextSearch map[string]string `yaml:"context_search" json:"context_search"`

transcripts/transcript_map.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ platforms:
101101
junos:
102102
vendor: "juniper"
103103
hostname: "cisshgo-junos"
104+
username: "admin"
104105
password: "admin"
105106
command_transcripts:
106107
"show version": "transcripts/juniper/junos/show_version.txt"

0 commit comments

Comments
 (0)