feat: add Smart Fridge shell and per-credential shell routing
Implement Samsung FridgeOS-themed shell (PLAN.md §3.3) with inventory management, temperature controls, diagnostics, alerts, and other appliance commands. Add per-credential shell routing so static credentials can specify which shell to use via the `shell` config field, passed through ssh.Permissions.Extensions. Also extract shared ReadLine helper from bash to the shell package so both shells can reuse terminal input handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ type credKey struct {
|
||||
type Decision struct {
|
||||
Accepted bool
|
||||
Reason string // "static_credential", "threshold_reached", "remembered_credential", "rejected"
|
||||
Shell string // optional: route to specific shell (only set for static credentials)
|
||||
}
|
||||
|
||||
type Authenticator struct {
|
||||
@@ -50,7 +51,7 @@ func (a *Authenticator) Authenticate(ip, username, password string) Decision {
|
||||
pMatch := subtle.ConstantTimeCompare([]byte(cred.Password), []byte(password))
|
||||
if uMatch == 1 && pMatch == 1 {
|
||||
a.failCounts[ip] = 0
|
||||
return Decision{Accepted: true, Reason: "static_credential"}
|
||||
return Decision{Accepted: true, Reason: "static_credential", Shell: cred.Shell}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,39 @@ func TestExpiredCredentialsSweep(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaticCredentialShellPropagation(t *testing.T) {
|
||||
a := newTestAuth(10, time.Hour,
|
||||
config.Credential{Username: "samsung", Password: "fridge", Shell: "fridge"},
|
||||
config.Credential{Username: "root", Password: "toor"},
|
||||
)
|
||||
|
||||
// Static credential with shell set should propagate it.
|
||||
d := a.Authenticate("1.2.3.4", "samsung", "fridge")
|
||||
if !d.Accepted || d.Reason != "static_credential" {
|
||||
t.Fatalf("got %+v, want accepted with static_credential", d)
|
||||
}
|
||||
if d.Shell != "fridge" {
|
||||
t.Errorf("Shell = %q, want %q", d.Shell, "fridge")
|
||||
}
|
||||
|
||||
// Static credential without shell should leave it empty.
|
||||
d = a.Authenticate("1.2.3.4", "root", "toor")
|
||||
if !d.Accepted || d.Reason != "static_credential" {
|
||||
t.Fatalf("got %+v, want accepted with static_credential", d)
|
||||
}
|
||||
if d.Shell != "" {
|
||||
t.Errorf("Shell = %q, want empty", d.Shell)
|
||||
}
|
||||
|
||||
// Threshold-reached decision should not have a shell set.
|
||||
a2 := newTestAuth(2, time.Hour)
|
||||
a2.Authenticate("5.5.5.5", "user", "pass")
|
||||
d = a2.Authenticate("5.5.5.5", "user", "pass")
|
||||
if d.Shell != "" {
|
||||
t.Errorf("threshold decision Shell = %q, want empty", d.Shell)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentAccess(t *testing.T) {
|
||||
a := newTestAuth(5, time.Hour)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
Reference in New Issue
Block a user