fa6d5bd3ba
Secrets defined once and applied to many workloads by scope (global or per-app), encrypted at rest and resolved into container env as a low-precedence default layer: global-shared < app-shared < image cfg.Env < workload_env. A workload with no applicable shared secrets is byte-identical to the prior workload_env-only behavior. - store: shared_secrets table + CRUD + ListApplicableSharedSecrets (enabled global + app, global-first), UNIQUE(scope,app_id,name). - plugin.ResolveSharedSecrets + integration into BuildWorkloadEnv (static/dockerfile) and image buildEnv; best-effort — a shared-secret store/decrypt error never fails a deploy, and values are never logged. - REST CRUD at /api/shared-secrets (reads authed, mutations AdminOnly); values encrypted at the boundary via crypto.Encrypt and never returned (only a has_value flag), mirroring workload_env. UNIQUE collisions 409. Compose is out of scope (YAML-defined env). Frontend rule UI is Phase 2. Reviewed: go + security APPROVE (0 CRITICAL/HIGH); two MEDIUMs fixed (translateSQLError -> 409, no driver-message leak). Deferred defense-in- depth: json:"-" on the model value + a description length cap.
219 lines
6.5 KiB
Go
219 lines
6.5 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestCreateSharedSecret_Validates(t *testing.T) {
|
|
s := newTestStore(t)
|
|
cases := []struct {
|
|
name string
|
|
in SharedSecret
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "missing name",
|
|
in: SharedSecret{Scope: SharedSecretScopeGlobal},
|
|
wantErr: "name is required",
|
|
},
|
|
{
|
|
name: "invalid scope",
|
|
in: SharedSecret{Name: "FOO", Scope: "team"},
|
|
wantErr: "invalid scope",
|
|
},
|
|
{
|
|
name: "app scope without app_id",
|
|
in: SharedSecret{Name: "FOO", Scope: SharedSecretScopeApp},
|
|
wantErr: "app_id is required",
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
_, err := s.CreateSharedSecret(c.in)
|
|
if err == nil {
|
|
t.Fatalf("expected error containing %q, got nil", c.wantErr)
|
|
}
|
|
if !strings.Contains(err.Error(), c.wantErr) {
|
|
t.Fatalf("error mismatch: got %q want substring %q", err.Error(), c.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateSharedSecret_GlobalForcesBlankAppID(t *testing.T) {
|
|
s := newTestStore(t)
|
|
got, err := s.CreateSharedSecret(SharedSecret{
|
|
Name: "GLOBAL_KEY", Value: "v", Scope: SharedSecretScopeGlobal,
|
|
AppID: "should-be-cleared", Enabled: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if got.AppID != "" {
|
|
t.Errorf("global secret AppID = %q, want empty", got.AppID)
|
|
}
|
|
if got.ID == "" {
|
|
t.Error("id should be set")
|
|
}
|
|
}
|
|
|
|
func TestCreateAndGetSharedSecret(t *testing.T) {
|
|
s := newTestStore(t)
|
|
created, err := s.CreateSharedSecret(SharedSecret{
|
|
Name: "API_KEY", Value: "ciphertext", Encrypted: true,
|
|
Scope: SharedSecretScopeApp, AppID: "app1", Description: "d", Enabled: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
got, err := s.GetSharedSecret(created.ID)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if got.Name != "API_KEY" || got.Value != "ciphertext" || !got.Encrypted {
|
|
t.Errorf("round-trip mismatch: %+v", got)
|
|
}
|
|
if got.Scope != SharedSecretScopeApp || got.AppID != "app1" {
|
|
t.Errorf("scope/app mismatch: %+v", got)
|
|
}
|
|
if !got.Enabled {
|
|
t.Error("enabled lost on round-trip")
|
|
}
|
|
}
|
|
|
|
func TestGetSharedSecret_NotFound(t *testing.T) {
|
|
s := newTestStore(t)
|
|
if _, err := s.GetSharedSecret("nope"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("expected ErrNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateSharedSecret(t *testing.T) {
|
|
s := newTestStore(t)
|
|
created, _ := s.CreateSharedSecret(SharedSecret{
|
|
Name: "K", Value: "v1", Scope: SharedSecretScopeGlobal, Enabled: true,
|
|
})
|
|
created.Value = "v2"
|
|
created.Description = "updated"
|
|
created.Enabled = false
|
|
got, err := s.UpdateSharedSecret(created)
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if got.Value != "v2" {
|
|
t.Errorf("value not updated: %q", got.Value)
|
|
}
|
|
if got.Description != "updated" {
|
|
t.Errorf("description not updated: %q", got.Description)
|
|
}
|
|
if got.Enabled {
|
|
t.Error("enabled=false not applied")
|
|
}
|
|
}
|
|
|
|
func TestUpdateSharedSecret_NotFound(t *testing.T) {
|
|
s := newTestStore(t)
|
|
_, err := s.UpdateSharedSecret(SharedSecret{
|
|
ID: "missing", Name: "K", Scope: SharedSecretScopeGlobal,
|
|
})
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("expected ErrNotFound updating missing secret, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteSharedSecret(t *testing.T) {
|
|
s := newTestStore(t)
|
|
created, _ := s.CreateSharedSecret(SharedSecret{
|
|
Name: "K", Value: "v", Scope: SharedSecretScopeGlobal, Enabled: true,
|
|
})
|
|
if err := s.DeleteSharedSecret(created.ID); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if _, err := s.GetSharedSecret(created.ID); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("expected ErrNotFound after delete, got %v", err)
|
|
}
|
|
if err := s.DeleteSharedSecret(created.ID); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("expected ErrNotFound deleting twice, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSharedSecret_UniquePerScopeAppName(t *testing.T) {
|
|
s := newTestStore(t)
|
|
if _, err := s.CreateSharedSecret(SharedSecret{
|
|
Name: "DUP", Value: "a", Scope: SharedSecretScopeGlobal, Enabled: true,
|
|
}); err != nil {
|
|
t.Fatalf("first create: %v", err)
|
|
}
|
|
// Same scope+name collides on the unique index.
|
|
if _, err := s.CreateSharedSecret(SharedSecret{
|
|
Name: "DUP", Value: "b", Scope: SharedSecretScopeGlobal, Enabled: true,
|
|
}); err == nil {
|
|
t.Fatal("expected unique-index violation for duplicate global key")
|
|
}
|
|
// Same name under an app scope is a distinct row.
|
|
if _, err := s.CreateSharedSecret(SharedSecret{
|
|
Name: "DUP", Value: "c", Scope: SharedSecretScopeApp, AppID: "app1", Enabled: true,
|
|
}); err != nil {
|
|
t.Fatalf("app-scoped same name should be allowed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestListApplicableSharedSecrets(t *testing.T) {
|
|
s := newTestStore(t)
|
|
// Two globals (one disabled), one app1 secret, one app2 secret.
|
|
mustCreate(t, s, SharedSecret{Name: "G_ONE", Value: "g1", Scope: SharedSecretScopeGlobal, Enabled: true})
|
|
mustCreate(t, s, SharedSecret{Name: "G_OFF", Value: "off", Scope: SharedSecretScopeGlobal, Enabled: false})
|
|
mustCreate(t, s, SharedSecret{Name: "A_ONE", Value: "a1", Scope: SharedSecretScopeApp, AppID: "app1", Enabled: true})
|
|
mustCreate(t, s, SharedSecret{Name: "A_TWO", Value: "a2", Scope: SharedSecretScopeApp, AppID: "app2", Enabled: true})
|
|
|
|
got, err := s.ListApplicableSharedSecrets("app1")
|
|
if err != nil {
|
|
t.Fatalf("applicable: %v", err)
|
|
}
|
|
// app1 sees the enabled global + its own; not the disabled global, not app2's.
|
|
if len(got) != 2 {
|
|
t.Fatalf("want 2 applicable secrets, got %d: %+v", len(got), got)
|
|
}
|
|
// Global must come first so callers can overlay app on top.
|
|
if got[0].Name != "G_ONE" {
|
|
t.Errorf("expected global first, got %q", got[0].Name)
|
|
}
|
|
if got[1].Name != "A_ONE" {
|
|
t.Errorf("expected app1 secret second, got %q", got[1].Name)
|
|
}
|
|
for _, sec := range got {
|
|
if sec.AppID == "app2" {
|
|
t.Errorf("app1 must not see app2's secret: %+v", sec)
|
|
}
|
|
if !sec.Enabled {
|
|
t.Errorf("disabled secret leaked into applicable set: %+v", sec)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestListApplicableSharedSecrets_NoAppOnlyGlobals(t *testing.T) {
|
|
s := newTestStore(t)
|
|
mustCreate(t, s, SharedSecret{Name: "G", Value: "g", Scope: SharedSecretScopeGlobal, Enabled: true})
|
|
mustCreate(t, s, SharedSecret{Name: "A", Value: "a", Scope: SharedSecretScopeApp, AppID: "app1", Enabled: true})
|
|
|
|
// An ungrouped workload (appID == "") sees only globals.
|
|
got, err := s.ListApplicableSharedSecrets("")
|
|
if err != nil {
|
|
t.Fatalf("applicable: %v", err)
|
|
}
|
|
if len(got) != 1 || got[0].Name != "G" {
|
|
t.Fatalf("ungrouped workload should see only the global, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func mustCreate(t *testing.T, s *Store, sec SharedSecret) SharedSecret {
|
|
t.Helper()
|
|
out, err := s.CreateSharedSecret(sec)
|
|
if err != nil {
|
|
t.Fatalf("create shared secret %q: %v", sec.Name, err)
|
|
}
|
|
return out
|
|
}
|