156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
package reconcile
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||
|
|
|
||
|
|
iamv1 "github.com/riotpiao/key-rotation-controller/api"
|
||
|
|
)
|
||
|
|
|
||
|
|
func rotation(last *metav1.Time, activeID string, hours int32) *iamv1.KeyRotation {
|
||
|
|
return &iamv1.KeyRotation{
|
||
|
|
Spec: iamv1.KeyRotationSpec{
|
||
|
|
Provider: iamv1.ProviderZitadelMachineKey,
|
||
|
|
Strategy: iamv1.StrategyOverlap,
|
||
|
|
IntervalHours: hours,
|
||
|
|
},
|
||
|
|
Status: iamv1.KeyRotationStatus{
|
||
|
|
LastRotation: last,
|
||
|
|
ActiveCredentialID: activeID,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRequeueAfterRotatesImmediatelyWhenNeverRotated(t *testing.T) {
|
||
|
|
r := &Reconciler{Now: func() time.Time { return time.Unix(1_700_000_000, 0) }}
|
||
|
|
if got := r.RequeueAfter(rotation(nil, "", 24)); got != 0 {
|
||
|
|
t.Fatalf("a resource with no rotation history must rotate now, got %s", got)
|
||
|
|
}
|
||
|
|
if got := r.RequeueAfter(rotation(nil, "key-1", 24)); got != 0 {
|
||
|
|
t.Fatalf("a resource with no lastRotation must rotate now, got %s", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRequeueAfterWaitsForTheInterval(t *testing.T) {
|
||
|
|
now := time.Unix(1_700_000_000, 0)
|
||
|
|
last := metav1.NewTime(now.Add(-6 * time.Hour))
|
||
|
|
r := &Reconciler{Now: func() time.Time { return now }}
|
||
|
|
|
||
|
|
got := r.RequeueAfter(rotation(&last, "key-1", 24))
|
||
|
|
want := 18 * time.Hour
|
||
|
|
if got != want {
|
||
|
|
t.Fatalf("expected %s until the next rotation, got %s", want, got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRequeueAfterRotatesWhenOverdue(t *testing.T) {
|
||
|
|
now := time.Unix(1_700_000_000, 0)
|
||
|
|
last := metav1.NewTime(now.Add(-30 * time.Hour))
|
||
|
|
r := &Reconciler{Now: func() time.Time { return now }}
|
||
|
|
|
||
|
|
if got := r.RequeueAfter(rotation(&last, "key-1", 24)); got != 0 {
|
||
|
|
t.Fatalf("an overdue resource must rotate now, got %s", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRequeueAfterFallsBackWhenNoInterval(t *testing.T) {
|
||
|
|
now := time.Unix(1_700_000_000, 0)
|
||
|
|
last := metav1.NewTime(now)
|
||
|
|
r := &Reconciler{Now: func() time.Time { return now }}
|
||
|
|
|
||
|
|
got := r.RequeueAfter(rotation(&last, "key-1", 0))
|
||
|
|
if got <= 0 {
|
||
|
|
t.Fatalf("a zero interval must fall back to a default, got %s", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSecretDataKeepsThePreviousValueUnderAShadowKey(t *testing.T) {
|
||
|
|
kr := &iamv1.KeyRotation{Spec: iamv1.KeyRotationSpec{
|
||
|
|
Target: iamv1.RotationTarget{
|
||
|
|
SecretKeys: map[string]string{"key": "jwt-key", "key-id": "jwt-key-id"},
|
||
|
|
},
|
||
|
|
}}
|
||
|
|
cred := &Credential{ID: "k2", Fields: map[string][]byte{
|
||
|
|
"key": []byte("PEM-2"),
|
||
|
|
"key-id": []byte("k2"),
|
||
|
|
}}
|
||
|
|
previous := map[string][]byte{
|
||
|
|
"jwt-key": []byte("PEM-1"),
|
||
|
|
"jwt-key-id": []byte("k1"),
|
||
|
|
}
|
||
|
|
|
||
|
|
data := secretData(kr, cred, previous)
|
||
|
|
|
||
|
|
if string(data["jwt-key"]) != "PEM-2" {
|
||
|
|
t.Fatalf("current value missing, got %q", data["jwt-key"])
|
||
|
|
}
|
||
|
|
if string(data["jwt-key"+previousKeySuffix]) != "PEM-1" {
|
||
|
|
t.Fatalf("previous value must be kept for rollback, got %q", data["jwt-key"+previousKeySuffix])
|
||
|
|
}
|
||
|
|
if string(data["jwt-key-id"+previousKeySuffix]) != "k1" {
|
||
|
|
t.Fatalf("previous key id must be kept for rollback")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSecretDataDefaultsToLogicalNames(t *testing.T) {
|
||
|
|
kr := &iamv1.KeyRotation{}
|
||
|
|
cred := &Credential{ID: "k1", Fields: map[string][]byte{"secret": []byte("s3cret")}}
|
||
|
|
|
||
|
|
data := secretData(kr, cred, nil)
|
||
|
|
|
||
|
|
if string(data["secret"]) != "s3cret" {
|
||
|
|
t.Fatalf("an unmapped field must keep its logical name, got %v", data)
|
||
|
|
}
|
||
|
|
if _, shadow := data["secret"+previousKeySuffix]; shadow {
|
||
|
|
t.Fatalf("no previous value existed, so no shadow key may be written")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSecretKeyForPrefersTheMapping(t *testing.T) {
|
||
|
|
kr := &iamv1.KeyRotation{Spec: iamv1.KeyRotationSpec{
|
||
|
|
Target: iamv1.RotationTarget{
|
||
|
|
SecretKeys: map[string]string{"secret": "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET"},
|
||
|
|
},
|
||
|
|
}}
|
||
|
|
if got := secretKeyFor(kr, "secret"); got != "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET" {
|
||
|
|
t.Fatalf("mapping not honoured, got %q", got)
|
||
|
|
}
|
||
|
|
if got := secretKeyFor(kr, "key-id"); got != "key-id" {
|
||
|
|
t.Fatalf("unmapped field must default to its own name, got %q", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMachineKeyProviderFieldsRespectTheMapping(t *testing.T) {
|
||
|
|
p := &MachineKeyProvider{}
|
||
|
|
kr := &iamv1.KeyRotation{Spec: iamv1.KeyRotationSpec{
|
||
|
|
Target: iamv1.RotationTarget{
|
||
|
|
SecretKeys: map[string]string{"key": "private-key", "key-id": "kid"},
|
||
|
|
},
|
||
|
|
}}
|
||
|
|
if p.keyField(kr) != "private-key" || p.keyIDField(kr) != "kid" {
|
||
|
|
t.Fatalf("provider must read the mapped Secret keys")
|
||
|
|
}
|
||
|
|
if p.Strategy() != iamv1.StrategyOverlap {
|
||
|
|
t.Fatalf("machine keys must be an overlapping provider")
|
||
|
|
}
|
||
|
|
if (&OIDCClientProvider{}).Strategy() != iamv1.StrategyAtomic {
|
||
|
|
t.Fatalf("client secrets cannot overlap and must be atomic")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestShortHashIsStableAndDoesNotLeakTheValue(t *testing.T) {
|
||
|
|
a := shortHash("super-secret-value")
|
||
|
|
b := shortHash("super-secret-value")
|
||
|
|
if a != b {
|
||
|
|
t.Fatalf("the marker must be stable for the same value")
|
||
|
|
}
|
||
|
|
if a == "super-secret-value" {
|
||
|
|
t.Fatalf("the marker must not be the value itself")
|
||
|
|
}
|
||
|
|
if shortHash("other") == a {
|
||
|
|
t.Fatalf("different values must produce different markers")
|
||
|
|
}
|
||
|
|
}
|