store : SQLite (clusters à secrets chiffrés, paramètres, utilisateurs, journal, audit)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015a8P6WMRPeqdpd5VCh6jAy
This commit is contained in:
1 parent
e61f2e4740
commit
2bbfb5dfbe
5 files changed
+1079
No files matched your search
@@ -0,0 +1,101 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testKey(b byte) []byte { return bytes.Repeat([]byte{b}, 32) }
|
||||
|
||||
func TestSecretsEncryptedAndKeyChecked(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", Reference: true, URL: "https://ref:8006", TokenID: "a@pve!ro", Secret: "s3cr3t-uuid"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var blob []byte
|
||||
if err := st.db.QueryRowContext(ctx, `SELECT secret FROM clusters WHERE name = 'ref'`).Scan(&blob); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Contains(blob, []byte("s3cr3t")) {
|
||||
t.Fatal("secret stored in clear")
|
||||
}
|
||||
cls, _ := st.Clusters(ctx)
|
||||
if len(cls) != 1 || cls[0].Secret != "" || !cls[0].HasSecret {
|
||||
t.Fatalf("listing must hide the secret: %+v", cls)
|
||||
}
|
||||
// an empty secret on update keeps the stored one
|
||||
c := cls[0]
|
||||
c.URL = "https://ref2:8006"
|
||||
if err := st.SaveCluster(ctx, "ref", c); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := st.Config(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s, _ := cfg.Reference.ResolveSecret(); s != "s3cr3t-uuid" {
|
||||
t.Fatalf("secret after update: %q", s)
|
||||
}
|
||||
exp, _ := st.ExportConfig(ctx)
|
||||
if exp.Reference.Secret != "" || exp.Reference.TokenSecretEnv != "PVE_REF_SECRET" {
|
||||
t.Fatalf("export: %+v", exp.Reference)
|
||||
}
|
||||
st.Close()
|
||||
if _, err := Open(path, testKey(2)); err == nil {
|
||||
t.Fatal("a wrong key must be refused")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user