Files
forge-tools-ssh/internal/ssh/policy_test.go
T

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)
}
}