Initial commit: forge-tools-ssh — MCP-сервер для администрирования по SSH
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user