package engine import ( "context" "encoding/json" "strings" "testing" "pve-confsync/internal/adapter" "pve-confsync/internal/config" "pve-confsync/internal/model" "pve-confsync/internal/pve/pvetest" ) const aliceHash = "$5$salt$aliceHashValue" func accessRef(t *testing.T) *pvetest.Server { ref := pvetest.New("9.1.4") t.Cleanup(ref.Close) ref.Groups["admins"] = map[string]any{"comment": "Admins"} ref.Groups["ops"] = map[string]any{"comment": "Ops"} ref.Roles["Ops"] = "VM.PowerMgmt,Sys.Audit" ref.Users["alice@pve"] = map[string]any{"enable": 1, "email": "alice@example.org", "groups": "ops,admins"} ref.Users["bob@pve"] = map[string]any{"enable": 1, "groups": "ops", "expire": 0} ref.Users["carol@pam"] = map[string]any{"enable": 1} ref.Shadow["alice"] = aliceHash ref.Shadow["bob"] = "$5$salt$bobHash" ref.Shadow["orphan"] = "$5$salt$orphan" // no account: left aside return ref } var accessKinds = []model.Kind{model.KindGroup, model.KindRole, model.KindUser, model.KindPassword} func TestAccountsAndPasswords(t *testing.T) { ctx := context.Background() ref := accessRef(t) cli := pvetest.New("9.0.10") defer cli.Close() cli.Users["alice@pve"] = map[string]any{"enable": 1, "email": "old@example.org", "firstname": "Al"} cli.Shadow["alice"] = "$5$salt$oldHash" cfg := &config.Config{Kinds: accessKinds, Reference: config.Cluster{Name: "ref"}, Clients: []config.Client{{Cluster: config.Cluster{Name: "site"}}}} if err := cfg.Validate(); err != nil { t.Fatal(err) } cl := &cfg.Clients[0] src, err := Read(ctx, client(t, ref, "ref"), cfg.Kinds, adapter.ReadRunning) if err != nil { t.Fatal(err) } for _, o := range src.Objects { if o.ID == "carol@pam" || o.ID == "orphan@pve" || o.ID == "Administrator" || o.ID == "root@pam" { t.Fatalf("%s must not be read", o.Key()) } } cc := client(t, cli, "site") cur, err := Read(ctx, cc, cfg.Kinds, adapter.ReadCurrent) if err != nil { t.Fatal(err) } plan := MakePlan(cfg, cl, src.Objects, cur.Objects) if len(plan.Errors) > 0 { t.Fatal(plan.Errors) } // 2 groups + 1 role + bob created, alice updated + 2 passwords if len(plan.Changes) != 7 { t.Fatalf("changes = %+v", plan.Changes) } b, _ := json.Marshal(plan) if strings.Contains(string(b), "aliceHash") || strings.Contains(string(b), "oldHash") { t.Fatalf("password hash in the plan JSON: %s", b) } res := Apply(ctx, cc, plan) if res.Err != nil { t.Fatalf("apply: %v (%+v)", res.Err, res.Steps) } if cli.Shadow["alice"] != aliceHash || cli.Shadow["bob"] != "$5$salt$bobHash" { t.Fatalf("shadow = %v", cli.Shadow) } a := cli.Users["alice@pve"] if a["email"] != "alice@example.org" || a["groups"] != "admins,ops" || a["firstname"] != nil { t.Fatalf("alice = %v", a) } if cli.Roles["Ops"] != "Sys.Audit,VM.PowerMgmt" { t.Fatalf("roles = %v", cli.Roles) } replan := func(actual []model.Object) Plan { return MakePlan(cfg, cl, src.Objects, actual) } if err := Verify(ctx, cc, replan, cfg.Kinds); err != nil { t.Fatalf("verify: %v", err) } } func TestPasswordSelectionPullsAccountAndGroups(t *testing.T) { ctx := context.Background() ref := accessRef(t) cli := pvetest.New("9.1.0") defer cli.Close() cfg := &config.Config{Kinds: accessKinds, Clients: []config.Client{{Cluster: config.Cluster{Name: "site"}}}} src, _ := Read(ctx, client(t, ref, "ref"), cfg.Kinds, adapter.ReadRunning) cur, _ := Read(ctx, client(t, cli, "site"), cfg.Kinds, adapter.ReadCurrent) p := Filter(MakePlan(cfg, &cfg.Clients[0], src.Objects, cur.Objects), []string{"pve-password/bob@pve"}) var keys []string for _, c := range p.Changes { keys = append(keys, c.Key()) } if got := strings.Join(keys, " "); got != "pve-group/ops pve-user/bob@pve pve-password/bob@pve" { t.Fatalf("selection = %s", got) } } func TestMissingParentIsAnError(t *testing.T) { ctx := context.Background() ref := accessRef(t) cli := pvetest.New("9.1.0") defer cli.Close() // groups excluded from the selection: alice cannot be created in them cfg := &config.Config{Kinds: accessKinds, Clients: []config.Client{{Cluster: config.Cluster{Name: "site"}}}, Objects: map[model.Kind]config.Selection{model.KindGroup: {Exclude: []string{"*"}}}} src, _ := Read(ctx, client(t, ref, "ref"), cfg.Kinds, adapter.ReadRunning) cur, _ := Read(ctx, client(t, cli, "site"), cfg.Kinds, adapter.ReadCurrent) plan := MakePlan(cfg, &cfg.Clients[0], src.Objects, cur.Objects) if len(plan.Errors) != 3 || !strings.Contains(strings.Join(plan.Errors, "\n"), "pve-user/alice@pve: pve-group/admins absent") { t.Fatalf("errors = %v", plan.Errors) } } func TestPasswordsNeedSSH(t *testing.T) { s := pvetest.New("9.1.0") defer s.Close() c, err := config.Cluster{Name: "c", URL: s.URL, Password: pvetest.Password, Insecure: true}.PVE() if err != nil { t.Fatal(err) } if _, err := Read(context.Background(), c, accessKinds, adapter.ReadCurrent); err == nil || !strings.Contains(err.Error(), "SSH") { t.Fatalf("err = %v", err) } }