253 lines
7.5 KiB
Go
253 lines
7.5 KiB
Go
package plugins
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/secret"
|
|
)
|
|
|
|
type runtimeConfigBackfillRows struct {
|
|
id int64
|
|
installationID int
|
|
key string
|
|
valueJSON []byte
|
|
yielded bool
|
|
}
|
|
|
|
func (r *runtimeConfigBackfillRows) Next() bool {
|
|
if r.yielded {
|
|
return false
|
|
}
|
|
r.yielded = true
|
|
return true
|
|
}
|
|
|
|
func (r *runtimeConfigBackfillRows) Scan(dest ...any) error {
|
|
if len(dest) != 4 {
|
|
return fmt.Errorf("scan destinations = %d, want 4", len(dest))
|
|
}
|
|
*dest[0].(*int64) = r.id
|
|
*dest[1].(*int) = r.installationID
|
|
*dest[2].(*string) = r.key
|
|
*dest[3].(*[]byte) = append([]byte(nil), r.valueJSON...)
|
|
return nil
|
|
}
|
|
|
|
func (r *runtimeConfigBackfillRows) Close() {}
|
|
func (r *runtimeConfigBackfillRows) Err() error { return nil }
|
|
func (r *runtimeConfigBackfillRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
|
|
func (r *runtimeConfigBackfillRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
|
|
func (r *runtimeConfigBackfillRows) Values() ([]any, error) { return nil, nil }
|
|
func (r *runtimeConfigBackfillRows) RawValues() [][]byte { return nil }
|
|
func (r *runtimeConfigBackfillRows) Conn() *pgx.Conn { return nil }
|
|
|
|
type runtimeConfigBackfillExec struct {
|
|
row runtimeConfigBackfillRows
|
|
updateTag pgconn.CommandTag
|
|
updateSQL string
|
|
updateArgs []any
|
|
}
|
|
|
|
func (e *runtimeConfigBackfillExec) Query(
|
|
_ context.Context,
|
|
_ string,
|
|
_ ...any,
|
|
) (pgx.Rows, error) {
|
|
row := e.row
|
|
return &row, nil
|
|
}
|
|
|
|
func (e *runtimeConfigBackfillExec) Exec(
|
|
_ context.Context,
|
|
sql string,
|
|
args ...any,
|
|
) (pgconn.CommandTag, error) {
|
|
e.updateSQL = sql
|
|
e.updateArgs = append([]any(nil), args...)
|
|
return e.updateTag, nil
|
|
}
|
|
|
|
func TestRuntimeConfigEncryptionRoundTrip(t *testing.T) {
|
|
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
|
|
if err != nil {
|
|
t.Fatalf("secret.New: %v", err)
|
|
}
|
|
value := map[string]any{"api_key": "clawrouter-e2e-secret", "enabled": true}
|
|
|
|
encoded, err := encodeRuntimeConfigValue(cipher, 42, "account", value)
|
|
if err != nil {
|
|
t.Fatalf("encodeRuntimeConfigValue: %v", err)
|
|
}
|
|
if bytes.Contains(encoded, []byte("clawrouter-e2e-secret")) {
|
|
t.Fatalf("encoded config contains plaintext secret: %s", encoded)
|
|
}
|
|
decoded, err := decodeRuntimeConfigValue(cipher, 42, "account", encoded)
|
|
if err != nil {
|
|
t.Fatalf("decodeRuntimeConfigValue: %v", err)
|
|
}
|
|
if decoded["api_key"] != "clawrouter-e2e-secret" || decoded["enabled"] != true {
|
|
t.Fatalf("decoded = %#v", decoded)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeConfigEncryptionBindsInstallationAndKey(t *testing.T) {
|
|
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
|
|
if err != nil {
|
|
t.Fatalf("secret.New: %v", err)
|
|
}
|
|
encoded, err := encodeRuntimeConfigValue(cipher, 42, "account", map[string]any{
|
|
"api_key": "clawrouter-e2e-secret",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encodeRuntimeConfigValue: %v", err)
|
|
}
|
|
if _, err := decodeRuntimeConfigValue(cipher, 43, "account", encoded); err == nil {
|
|
t.Fatal("decode with a different installation id succeeded")
|
|
}
|
|
if _, err := decodeRuntimeConfigValue(cipher, 42, "other", encoded); err == nil {
|
|
t.Fatal("decode with a different config key succeeded")
|
|
}
|
|
}
|
|
|
|
func TestRuntimeConfigLegacyPlaintextRemainsReadable(t *testing.T) {
|
|
decoded, err := decodeRuntimeConfigValue(nil, 1, "account", []byte(`{"api_key":"clawrouter-e2e-secret"}`))
|
|
if err != nil {
|
|
t.Fatalf("decodeRuntimeConfigValue: %v", err)
|
|
}
|
|
if decoded["api_key"] != "clawrouter-e2e-secret" {
|
|
t.Fatalf("decoded = %#v", decoded)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeConfigUnrelatedEditPreservesLargeInteger(t *testing.T) {
|
|
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
|
|
if err != nil {
|
|
t.Fatalf("secret.New: %v", err)
|
|
}
|
|
const largeInteger = "9007199254740993"
|
|
encoded, err := encodeRuntimeConfigJSON(
|
|
cipher,
|
|
42,
|
|
"account",
|
|
[]byte(`{"cursor":`+largeInteger+`,"display_name":"old"}`),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("encodeRuntimeConfigJSON: %v", err)
|
|
}
|
|
|
|
decoded, err := decodeRuntimeConfigValue(cipher, 42, "account", encoded)
|
|
if err != nil {
|
|
t.Fatalf("decodeRuntimeConfigValue: %v", err)
|
|
}
|
|
cursor, ok := decoded["cursor"].(json.Number)
|
|
if !ok || cursor.String() != largeInteger {
|
|
t.Fatalf("decoded cursor = %#v, want json.Number(%s)", decoded["cursor"], largeInteger)
|
|
}
|
|
decoded["display_name"] = "new"
|
|
|
|
reencoded, err := encodeRuntimeConfigValue(cipher, 42, "account", decoded)
|
|
if err != nil {
|
|
t.Fatalf("encodeRuntimeConfigValue: %v", err)
|
|
}
|
|
var envelope map[string]string
|
|
if err := json.Unmarshal(reencoded, &envelope); err != nil {
|
|
t.Fatalf("unmarshal encrypted envelope: %v", err)
|
|
}
|
|
plaintext, err := cipher.Decrypt(
|
|
envelope[encryptedRuntimeConfigField],
|
|
runtimeConfigAAD(42, "account"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("decrypt edited value: %v", err)
|
|
}
|
|
var raw map[string]json.RawMessage
|
|
if err := json.Unmarshal([]byte(plaintext), &raw); err != nil {
|
|
t.Fatalf("unmarshal edited plaintext: %v", err)
|
|
}
|
|
if string(raw["cursor"]) != largeInteger {
|
|
t.Fatalf("edited cursor = %s, want %s", raw["cursor"], largeInteger)
|
|
}
|
|
if string(raw["display_name"]) != `"new"` {
|
|
t.Fatalf("edited display_name = %s, want new", raw["display_name"])
|
|
}
|
|
}
|
|
|
|
func TestRuntimeConfigEncryptionBackfillPreservesLargeInteger(t *testing.T) {
|
|
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
|
|
if err != nil {
|
|
t.Fatalf("secret.New: %v", err)
|
|
}
|
|
legacy := []byte(`{"cursor":9007199254740993}`)
|
|
db := &runtimeConfigBackfillExec{
|
|
row: runtimeConfigBackfillRows{
|
|
id: 9,
|
|
installationID: 42,
|
|
key: "account",
|
|
valueJSON: legacy,
|
|
},
|
|
updateTag: pgconn.NewCommandTag("UPDATE 1"),
|
|
}
|
|
|
|
updated, err := backfillEncryptedConfigs(context.Background(), db, cipher)
|
|
if err != nil {
|
|
t.Fatalf("backfillEncryptedConfigs: %v", err)
|
|
}
|
|
if updated != 1 {
|
|
t.Fatalf("updated = %d, want 1", updated)
|
|
}
|
|
encoded := db.updateArgs[1].([]byte)
|
|
var envelope map[string]string
|
|
if err := json.Unmarshal(encoded, &envelope); err != nil {
|
|
t.Fatalf("unmarshal encrypted envelope: %v", err)
|
|
}
|
|
plaintext, err := cipher.Decrypt(
|
|
envelope[encryptedRuntimeConfigField],
|
|
runtimeConfigAAD(42, "account"),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("decrypt backfilled value: %v", err)
|
|
}
|
|
if plaintext != string(legacy) {
|
|
t.Fatalf("backfilled plaintext = %s, want %s", plaintext, legacy)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeConfigEncryptionBackfillSkipsConcurrentWrite(t *testing.T) {
|
|
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
|
|
if err != nil {
|
|
t.Fatalf("secret.New: %v", err)
|
|
}
|
|
legacy := []byte(`{"region":"original"}`)
|
|
db := &runtimeConfigBackfillExec{
|
|
row: runtimeConfigBackfillRows{
|
|
id: 9,
|
|
installationID: 42,
|
|
key: "account",
|
|
valueJSON: legacy,
|
|
},
|
|
updateTag: pgconn.NewCommandTag("UPDATE 0"),
|
|
}
|
|
|
|
updated, err := backfillEncryptedConfigs(context.Background(), db, cipher)
|
|
if err != nil {
|
|
t.Fatalf("backfillEncryptedConfigs: %v", err)
|
|
}
|
|
if updated != 0 {
|
|
t.Fatalf("updated = %d, want 0 after concurrent replacement", updated)
|
|
}
|
|
if !strings.Contains(db.updateSQL, "config_value = $3::jsonb") {
|
|
t.Fatalf("query predicate missing: %s", db.updateSQL)
|
|
}
|
|
if len(db.updateArgs) != 3 || !bytes.Equal(db.updateArgs[2].([]byte), legacy) {
|
|
t.Fatal("third query argument mismatch")
|
|
}
|
|
}
|