mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
Local vault (apps/cli): clean-room PRMVLT01 format — Argon2id → XChaCha20-Poly1305, AEAD-bound header, atomic writes. vault:* / secrets:* / inject (primora:// refs) / agent credential broker (dummy tokens in the child, real secret on the wire) / TTL sessions / stack:* operator group. Project vault (backend): core.project_secrets, AES-256-GCM via the platform encryptor. Metadata-only list, audit-logged reveal, and secret://NAME refs resolved at job delivery — refs stored, values never. Surfaces: dashboard Vault page (+ local-vault card under the Tauri shell), desktop vault window via IPC to the primora binary, secrets:* --remote CLI mode, metadata/write-only MCP tools so agents consume secrets via secret:// refs without holding plaintext. Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
126 lines
4.0 KiB
Go
126 lines
4.0 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
db "github.com/tdvorak/primora/apps/backend/internal/database/db"
|
|
"github.com/tdvorak/primora/apps/backend/internal/secrets"
|
|
)
|
|
|
|
func testEncryptor(t *testing.T) *secrets.Encryptor {
|
|
t.Helper()
|
|
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
|
enc, err := secrets.NewEncryptor(key)
|
|
if err != nil {
|
|
t.Fatalf("NewEncryptor: %v", err)
|
|
}
|
|
return enc
|
|
}
|
|
|
|
func secretLister(t *testing.T, enc *secrets.Encryptor, kv map[string]string) func(context.Context, uuid.UUID) ([]db.ListProjectSecretValuesRow, error) {
|
|
t.Helper()
|
|
return func(context.Context, uuid.UUID) ([]db.ListProjectSecretValuesRow, error) {
|
|
rows := make([]db.ListProjectSecretValuesRow, 0, len(kv))
|
|
for name, value := range kv {
|
|
ct, err := enc.Encrypt([]byte(value))
|
|
if err != nil {
|
|
t.Fatalf("encrypt %s: %v", name, err)
|
|
}
|
|
rows = append(rows, db.ListProjectSecretValuesRow{Name: name, Ciphertext: ct})
|
|
}
|
|
return rows, nil
|
|
}
|
|
}
|
|
|
|
func TestResolveSecretRefs(t *testing.T) {
|
|
enc := testEncryptor(t)
|
|
list := secretLister(t, enc, map[string]string{
|
|
"API_KEY": "sk_live_99",
|
|
"RECURSIVE": "secret://API_KEY", // must not resolve twice
|
|
})
|
|
pid := uuid.New()
|
|
|
|
t.Run("replaces nested and array refs", func(t *testing.T) {
|
|
payload := json.RawMessage(`{
|
|
"token": "secret://API_KEY",
|
|
"nested": {"deep": ["a", "secret://API_KEY", {"x": "secret://API_KEY"}]},
|
|
"plain": "not a secret",
|
|
"num": 42
|
|
}`)
|
|
out, err := resolveSecretRefs(context.Background(), list, enc, pid, payload)
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
var doc map[string]any
|
|
if err := json.Unmarshal(out, &doc); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if doc["token"] != "sk_live_99" {
|
|
t.Errorf("token = %v", doc["token"])
|
|
}
|
|
deep := doc["nested"].(map[string]any)["deep"].([]any)
|
|
if deep[1] != "sk_live_99" || deep[2].(map[string]any)["x"] != "sk_live_99" {
|
|
t.Errorf("nested refs not resolved: %v", deep)
|
|
}
|
|
if doc["plain"] != "not a secret" || doc["num"] != float64(42) {
|
|
t.Errorf("non-ref values changed: %v", doc)
|
|
}
|
|
})
|
|
|
|
t.Run("missing ref fails the job", func(t *testing.T) {
|
|
_, err := resolveSecretRefs(context.Background(), list, enc, pid, json.RawMessage(`{"a":"secret://NOPE"}`))
|
|
if err == nil || !strings.Contains(err.Error(), "NOPE") {
|
|
t.Fatalf("expected unresolved error naming NOPE, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("embedded ref in a larger string is not resolved", func(t *testing.T) {
|
|
out, err := resolveSecretRefs(context.Background(), list, enc, pid, json.RawMessage(`{"a":"prefix secret://API_KEY suffix"}`))
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
if !strings.Contains(string(out), "secret://API_KEY") {
|
|
t.Fatalf("embedded ref must pass through unchanged: %s", out)
|
|
}
|
|
})
|
|
|
|
t.Run("resolved value is not re-scanned", func(t *testing.T) {
|
|
out, err := resolveSecretRefs(context.Background(), list, enc, pid, json.RawMessage(`{"a":"secret://RECURSIVE"}`))
|
|
if err != nil {
|
|
t.Fatalf("resolve: %v", err)
|
|
}
|
|
var doc map[string]any
|
|
_ = json.Unmarshal(out, &doc)
|
|
if doc["a"] != "secret://API_KEY" {
|
|
t.Fatalf("single-pass resolution expected, got %v", doc["a"])
|
|
}
|
|
})
|
|
|
|
t.Run("invalid JSON fails loudly when a ref is present", func(t *testing.T) {
|
|
_, err := resolveSecretRefs(context.Background(), list, enc, pid, json.RawMessage(`{secret://API_KEY`))
|
|
if err == nil {
|
|
t.Fatal("expected JSON error")
|
|
}
|
|
})
|
|
|
|
t.Run("payloads without refs pass through untouched", func(t *testing.T) {
|
|
payload := json.RawMessage(`{"a":1}`)
|
|
out, err := resolveSecretRefs(context.Background(), list, enc, pid, payload)
|
|
if err != nil || string(out) != string(payload) {
|
|
t.Fatalf("passthrough failed: %v %s", err, out)
|
|
}
|
|
// Nil encryptor must fail loudly — a literal secret:// reaching the
|
|
// endpoint is worse than a failed run.
|
|
_, err = resolveSecretRefs(context.Background(), list, nil, pid, json.RawMessage(`{"a":"secret://API_KEY"}`))
|
|
if err == nil {
|
|
t.Fatal("expected error when encryption is not configured")
|
|
}
|
|
})
|
|
}
|