package pve import ( "testing" ) // include: unit-тесты политики/конфига домена (без сети и без MCP). func TestParseConfigDefaults(t *testing.T) { raw := []byte(`{ "hosts": [{"alias":"pve","url":"https://10.0.0.5:8006/api2/json","token_id":"u@pve!mcp","token_secret":"S"}] }`) cfg, err := ParseConfig(raw) if err != nil { t.Fatalf("ParseConfig: %v", err) } if !cfg.IsReadOnly() { t.Error("default ReadOnly should be true") } if cfg.Default != "pve" { t.Errorf("default host = %q, want pve", cfg.Default) } if !cfg.HostExists("pve") { t.Error("pve should exist") } if cfg.MultiHost() { t.Error("single host should not be multi") } } func TestParseConfigNoHostsFails(t *testing.T) { if _, err := ParseConfig([]byte(`{}`)); err == nil { t.Fatal("expected error for config without hosts") } } func TestParseConfigMissingTokenFails(t *testing.T) { raw := []byte(`{ "hosts":[{"alias":"pve","url":"https://10.0.0.5:8006/api2/json"}] }`) if _, err := ParseConfig(raw); err == nil { t.Fatal("expected fail-closed on missing token") } } func TestParseConfigUnexpandedVarFails(t *testing.T) { // ${VAR} отсутствует в окружении => token_secret пустой => fail-closed. raw := []byte(`{ "hosts":[{"alias":"pve","url":"https://10.0.0.5:8006/api2/json","token_id":"m","token_secret":"${DEFINITELY_MISSING_VAR}"}] }`) if _, err := ParseConfig(raw); err == nil { t.Fatal("expected fail-closed on unexpanded secret var") } } func TestParseConfigDuplicateAliasFails(t *testing.T) { raw := []byte(`{ "hosts":[ {"alias":"pve","url":"https://10.0.0.1:8006/api2/json","token_id":"a","token_secret":"s"}, {"alias":"pve","url":"https://10.0.0.2:8006/api2/json","token_id":"b","token_secret":"t"} ]}`) if _, err := ParseConfig(raw); err == nil { t.Fatal("expected fail on duplicate alias") } } func TestParseConfigDuplicateURLFails(t *testing.T) { raw := []byte(`{ "hosts":[ {"alias":"a","url":"https://10.0.0.1:8006/api2/json","token_id":"a","token_secret":"s"}, {"alias":"b","url":"https://10.0.0.1:8006/api2/json","token_id":"b","token_secret":"t"} ]}`) if _, err := ParseConfig(raw); err == nil { t.Fatal("expected fail on duplicate url") } } func TestParseConfigMultiHostGlobalAllowlistFails(t *testing.T) { // Мульти-гипервизор + глобальный allowlist = коллизия VMID → fail-closed. raw := []byte(`{ "hosts":[ {"alias":"a","url":"https://10.0.0.1:8006/api2/json","token_id":"a","token_secret":"s"}, {"alias":"b","url":"https://10.0.0.2:8006/api2/json","token_id":"b","token_secret":"t"} ], "allowlist":{"vmids":[100]}}`) if _, err := ParseConfig(raw); err == nil { t.Fatal("expected fail on global allowlist in multi-host config") } } // Ключевой тест коллизии VMID: один и тот же vmid на разных хостах // обязан давать РАЗНЫЙ вердикт по авторизации. func TestWriteAllowed_NoCrossHostLeak(t *testing.T) { cfg := &Config{ Hosts: []HostConfig{ {Alias: "a", URL: "https://10.0.0.1:8006/api2/json", TokenID: "u", TokenSecret: "s", AllowVMIDs: []int{500}}, {Alias: "b", URL: "https://10.0.0.2:8006/api2/json", TokenID: "u", TokenSecret: "s", AllowVMIDs: []int{200}}, }, ReadOnly: boolPtr(false), } if !cfg.WriteAllowed("a", 500) { t.Error("host a vmid 500 should be allowed") } if cfg.WriteAllowed("b", 500) { t.Error("host b vmid 500 must be DENIED (not in its allowlist) — no cross-host leak") } if !cfg.WriteAllowed("b", 200) { t.Error("host b vmid 200 should be allowed") } } func TestWriteAllowedFailClosedEmpty(t *testing.T) { cfg := &Config{Hosts: []HostConfig{{Alias: "a", URL: "https://10.0.0.1:8006/api2/json", TokenID: "u", TokenSecret: "s"}}, ReadOnly: boolPtr(false)} if cfg.WriteAllowed("a", 500) { t.Error("empty allow_vmids must fail-closed (deny writes)") } } func TestWriteAllowedReadOnlyDenies(t *testing.T) { cfg := &Config{Hosts: []HostConfig{{Alias: "a", URL: "https://10.0.0.1:8006/api2/json", TokenID: "u", TokenSecret: "s", AllowVMIDs: []int{500}}}, ReadOnly: boolPtr(true)} if cfg.WriteAllowed("a", 500) { t.Error("read_only must deny writes even if allowlist set") } } func TestWriteAllowedGlobalFallbackSingleHost(t *testing.T) { // Одиночный гипервизор: глобальный allowlist — допустимый фолбэк. cfg := &Config{ Hosts: []HostConfig{{Alias: "a", URL: "https://10.0.0.1:8006/api2/json", TokenID: "u", TokenSecret: "s"}}, Allowlist: Allowlist{VMIDs: []int{500}}, ReadOnly: boolPtr(false), } if !cfg.WriteAllowed("a", 500) { t.Error("global allowlist should fall back for single host") } if cfg.WriteAllowed("a", 100) { t.Error("vmid 100 should be denied") } } func TestNodeWriteAllowed(t *testing.T) { cfg := &Config{ Hosts: []HostConfig{{Alias: "a", URL: "https://10.0.0.1:8006/api2/json", TokenID: "u", TokenSecret: "s", AllowNodes: []string{"pve"}}}, ReadOnly: boolPtr(false), } if !cfg.NodeWriteAllowed("a", "pve") { t.Error("node pve should be allowed") } if cfg.NodeWriteAllowed("a", "other") { t.Error("node other should be denied") } } func TestDenyConfigKeys(t *testing.T) { cfg := &Config{denyConfigKeys: DefaultDenyConfigKeys} for _, k := range []string{"delete", "revert", "hotplug"} { if !cfg.DenyConfigKey(k) { t.Errorf("key %q should be denied", k) } } if cfg.DenyConfigKey("name") { t.Error("name should NOT be denied") } } func TestIsValidHTTPURL(t *testing.T) { valid := []string{"https://pve.local:8006/api2/json", "http://10.0.0.1:8006/api2/json"} for _, u := range valid { if !isValidHTTPURL(u) { t.Errorf("expected valid: %s", u) } } invalid := []string{"", "ftp://x", "https://", "javascript:alert(1)"} for _, u := range invalid { if isValidHTTPURL(u) { t.Errorf("expected invalid: %s", u) } } } func TestValidateIdentifier(t *testing.T) { ok := []string{"pve", "pve1", "snap-name", "104", "local-lvm", "scsi0"} for _, s := range ok { if err := ValidateIdentifier(s); err != nil { t.Errorf("expected valid %q: %v", s, err) } } bad := []string{"", "a/b", "..", "a b", "a;rm", "a\\b", "a\x00b", "a:b", "a@b"} for _, s := range bad { if err := ValidateIdentifier(s); err == nil { t.Errorf("expected invalid %q", s) } } } func TestValidateUPID(t *testing.T) { ok := "UPID:pve:00000000:root@pam!mcp:1:2:3:qemu:100:abc" if err := ValidateUPID(ok); err != nil { t.Errorf("expected valid upid %q: %v", ok, err) } bad := []string{"", "a/b", "..", "a\\b", "a\x00b"} for _, s := range bad { if err := ValidateUPID(s); err == nil { t.Errorf("expected invalid upid %q", s) } } } func boolPtr(b bool) *bool { return &b }