118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package ssh
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writePolicy(t *testing.T, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "ssh.json")
|
|
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("write policy: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestLoadPolicy_MissingFileNil(t *testing.T) {
|
|
p, err := LoadPolicy(filepath.Join(t.TempDir(), "nope.json"))
|
|
if err != nil {
|
|
t.Fatalf("missing file: %v", err)
|
|
}
|
|
if p != nil {
|
|
t.Fatalf("expected nil policy for missing file, got %+v", p)
|
|
}
|
|
}
|
|
|
|
func TestLoadPolicy_ExpandsVars(t *testing.T) {
|
|
t.Setenv("FORGE_SSH_KEY", "/keys/agent")
|
|
p, err := LoadPolicy(writePolicy(t, `{
|
|
"profiles":[{"alias":"prod","host":"10.0.1.5","username":"deploy","key":"${FORGE_SSH_KEY}"}],
|
|
"allowed_hosts":["10.0.*"],
|
|
"default_key_path":"${FORGE_SSH_KEY}"
|
|
}`))
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
pr, ok := p.Resolve("prod")
|
|
if !ok {
|
|
t.Fatal("profile prod not resolved")
|
|
}
|
|
if pr.Host != "10.0.1.5" || pr.KeyPath != "/keys/agent" {
|
|
t.Fatalf("profile = %+v", pr)
|
|
}
|
|
if p.DefaultKeyPath != "/keys/agent" {
|
|
t.Fatalf("default_key = %q", p.DefaultKeyPath)
|
|
}
|
|
}
|
|
|
|
func TestPolicy_Allowlist(t *testing.T) {
|
|
p, err := LoadPolicy(writePolicy(t, `{"allowed_hosts":["10.0.*","*.internal"]}`))
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if !p.Allow("10.0.1.5") || !p.Allow("db.internal") {
|
|
t.Fatal("expected allowed hosts to pass")
|
|
}
|
|
if p.Allow("192.168.0.1") || p.Allow("evil.com") {
|
|
t.Fatal("expected non-allowed hosts to be denied")
|
|
}
|
|
}
|
|
|
|
func TestPolicy_NoAllowlistAllowsAll(t *testing.T) {
|
|
p, err := LoadPolicy(writePolicy(t, `{"profiles":[{"alias":"a","host":"h","username":"u"}]}`))
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if !p.Allow("anything.example") {
|
|
t.Fatal("empty allowlist should allow all (backward compat)")
|
|
}
|
|
}
|
|
|
|
// testManager изолирует генерацию ключа в temp-каталог, чтобы тесты не
|
|
// создавали ./data/id_ed25519 в модуле.
|
|
func testManager(t *testing.T) *Manager {
|
|
t.Helper()
|
|
t.Setenv("SSH_MCP_KEY_PATH", filepath.Join(t.TempDir(), "id_ed25519"))
|
|
return NewManager("")
|
|
}
|
|
|
|
func TestConnect_ProfileResolution_DeniesUnknown(t *testing.T) {
|
|
m := testManager(t)
|
|
m.SetPolicy(&Policy{
|
|
Profiles: []Profile{{Alias: "prod", Host: "10.0.1.5", Username: "deploy"}},
|
|
})
|
|
|
|
// Неизвестный профиль не должен уходить в сеть.
|
|
_, err := m.Connect(context.Background(), ConnectOptions{Profile: "nope"})
|
|
if err == nil || !strings.Contains(err.Error(), "not found") {
|
|
t.Fatalf("expected profile-not-found error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConnect_Allowlist_DeniesHost(t *testing.T) {
|
|
m := testManager(t)
|
|
m.SetPolicy(&Policy{AllowedHosts: []string{"10.0.*"}})
|
|
|
|
// Host вне allowlist - deny до dial.
|
|
_, err := m.Connect(context.Background(), ConnectOptions{Host: "evil.example", Username: "x"})
|
|
if err == nil || !strings.Contains(err.Error(), "not in allowed_hosts") {
|
|
t.Fatalf("expected allowlist deny, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestConnect_ProfileWithoutPolicy_Denies(t *testing.T) {
|
|
m := testManager(t)
|
|
_, err := m.Connect(context.Background(), ConnectOptions{Profile: "prod"})
|
|
if err == nil {
|
|
t.Fatal("expected error when profile used without policy")
|
|
}
|
|
if !strings.Contains(err.Error(), "per-agent policy") {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|