package store import ( "bytes" "context" "database/sql" "path/filepath" "strings" "testing" ) func testKey(b byte) []byte { return bytes.Repeat([]byte{b}, 32) } func TestPasswordsEncryptedAndKeyChecked(t *testing.T) { ctx := context.Background() path := filepath.Join(t.TempDir(), "t.db") st, err := Open(path, testKey(1)) if err != nil { t.Fatal(err) } if err := st.SaveCluster(ctx, "", Cluster{Name: "ref", Role: ClusterPrimary, URL: "https://ref:8006", Password: "s3cr3t-root"}); err != nil { t.Fatal(err) } var blob []byte if err := st.db.QueryRowContext(ctx, `SELECT password FROM clusters WHERE name = 'ref'`).Scan(&blob); err != nil { t.Fatal(err) } if bytes.Contains(blob, []byte("s3cr3t")) { t.Fatal("password stored in clear") } cls, _ := st.Clusters(ctx) if len(cls) != 1 || cls[0].Password != "" || !cls[0].HasPassword || cls[0].User != "root@pam" || cls[0].TLS != TLSPin { t.Fatalf("listing must hide the password: %+v", cls) } // an empty password on update keeps the stored one c := cls[0] c.Timeout = "20s" if err := st.SaveCluster(ctx, "ref", c); err != nil { t.Fatal(err) } cfg, err := st.Config(ctx) if err != nil { t.Fatal(err) } if cfg.Reference.Password != "s3cr3t-root" || cfg.Reference.User != "root@pam" { t.Fatalf("password after update: %+v", cfg.Reference) } st.Close() if _, err := Open(path, testKey(2)); err == nil { t.Fatal("a wrong key must be refused") } } func TestRolesAndPins(t *testing.T) { ctx := context.Background() st, err := Open(filepath.Join(t.TempDir(), "t.db"), testKey(1)) if err != nil { t.Fatal(err) } defer st.Close() if _, err := st.Config(ctx); err != ErrNoReference { t.Fatalf("empty database: %v", err) } for _, n := range []string{"a", "b", "c"} { if err := st.SaveCluster(ctx, "", Cluster{Name: n, URL: "https://" + n + ":8006", Password: "pw"}); err != nil { t.Fatal(err) } } if err := st.SetRole(ctx, "a", ClusterPrimary); err != nil { t.Fatal(err) } if err := st.SetRole(ctx, "c", ClusterDisabled); err != nil { t.Fatal(err) } if err := st.SetRole(ctx, "b", ClusterPrimary); err != nil { // a becomes a target t.Fatal(err) } cfg, err := st.Config(ctx) if err != nil { t.Fatal(err) } if cfg.Reference.Name != "b" || len(cfg.Clients) != 1 || cfg.Clients[0].Name != "a" { t.Fatalf("primary %s, targets %+v", cfg.Reference.Name, cfg.Clients) } if err := st.Pin(ctx, "a", "AA:BB", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl", &Check{OK: true}); err != nil { t.Fatal(err) } _, cc, _ := st.Cluster(ctx, "a") if cc.Fingerprint != "AA:BB" || cc.SSHHostKey == "" { t.Fatalf("pins = %+v", cc) } // another address: the pins are forgotten c, _, _ := st.Cluster(ctx, "a") c.URL = "https://a2:8006" if err := st.SaveCluster(ctx, "a", c); err != nil { t.Fatal(err) } _, cc, _ = st.Cluster(ctx, "a") if cc.Fingerprint != "" || cc.SSHHostKey != "" { t.Fatalf("pins kept after an address change: %+v", cc) } } func TestFirstAdmin(t *testing.T) { ctx := context.Background() st, err := Open(filepath.Join(t.TempDir(), "t.db"), testKey(1)) if err != nil { t.Fatal(err) } defer st.Close() if err := st.CreateFirstAdmin(ctx, "admin", "too-short"); err == nil { t.Fatal("short password accepted") } if err := st.CreateFirstAdmin(ctx, "admin", "a-long-password"); err != nil { t.Fatal(err) } if err := st.CreateFirstAdmin(ctx, "other", "a-long-password"); err != ErrSetupDone { t.Fatalf("second setup: %v", err) } } func TestUsers(t *testing.T) { ctx := context.Background() st, err := Open(filepath.Join(t.TempDir(), "t.db"), testKey(1)) if err != nil { t.Fatal(err) } defer st.Close() if err := st.SaveUser(ctx, "admin", RoleAdmin, "short"); err == nil { t.Fatal("short password accepted") } if err := st.SaveUser(ctx, "admin", RoleAdmin, "long-enough"); err != nil { t.Fatal(err) } if _, ok := st.Authenticate(ctx, "admin", "wrong-pass"); ok { t.Fatal("wrong password accepted") } u, ok := st.Authenticate(ctx, "admin", "long-enough") if !ok || u.Role != RoleAdmin { t.Fatalf("login: %+v %v", u, ok) } if err := st.SaveUser(ctx, "admin", RoleViewer, ""); err == nil { t.Fatal("last admin demoted") } if err := st.DeleteUser(ctx, "admin"); err == nil { t.Fatal("last admin deleted") } // a new password changes the epoch (open sessions are invalidated) if err := st.SaveUser(ctx, "admin", RoleAdmin, "another-one"); err != nil { t.Fatal(err) } u2, _ := st.User(ctx, "admin") if u2.Epoch == u.Epoch { t.Fatal("epoch unchanged after a password change") } } func TestPasswordHash(t *testing.T) { h, err := HashPassword("p@ssw0rd!") if err != nil { t.Fatal(err) } if strings.Contains(h, "p@ssw0rd") || !CheckPassword(h, "p@ssw0rd!") || CheckPassword(h, "p@ssw0rd") { t.Fatal("hash check") } } func TestMigrateFromTokens(t *testing.T) { path := filepath.Join(t.TempDir(), "t.db") db, err := sql.Open("sqlite", "file:"+path) if err != nil { t.Fatal(err) } if _, err := db.Exec(`CREATE TABLE clusters (name TEXT PRIMARY KEY, reference INTEGER, url TEXT, token_id TEXT, secret BLOB, updated_at INTEGER); INSERT INTO clusters VALUES ('old', 1, 'https://x:8006', 'a@pve!t', x'01', 0); CREATE TABLE users (name TEXT PRIMARY KEY, role TEXT NOT NULL, password TEXT NOT NULL, epoch INTEGER NOT NULL, created_at INTEGER NOT NULL, last_login INTEGER NOT NULL DEFAULT 0); INSERT INTO users VALUES ('admin', 'admin', 'h', 1, 1, 0)`); err != nil { t.Fatal(err) } db.Close() st, err := Open(path, testKey(1)) if err != nil { t.Fatal(err) } defer st.Close() cls, err := st.Clusters(context.Background()) if err != nil || len(cls) != 0 { t.Fatalf("clusters after migration: %v %v", cls, err) } if n, _ := st.CountUsers(context.Background()); n != 1 { t.Fatal("users must be kept") } }