| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712 |
- /*
- Copyright © The ESO Authors
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- https://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package crd
- import (
- "context"
- "encoding/json"
- "errors"
- "strings"
- "testing"
- apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
- "k8s.io/apimachinery/pkg/api/meta"
- "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- kclient "sigs.k8s.io/controller-runtime/pkg/client"
- crfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
- )
- // ── test doubles ─────────────────────────────────────────────────────────────
- type testPushSecretData struct{}
- func (testPushSecretData) GetMetadata() *apiextensionsv1.JSON { return nil }
- func (testPushSecretData) GetSecretKey() string { return "" }
- func (testPushSecretData) GetRemoteKey() string { return "" }
- func (testPushSecretData) GetProperty() string { return "" }
- type testPushSecretRemoteRef struct {
- remoteKey string
- property string
- }
- func (r testPushSecretRemoteRef) GetRemoteKey() string { return r.remoteKey }
- func (r testPushSecretRemoteRef) GetProperty() string { return r.property }
- // ── helpers ──────────────────────────────────────────────────────────────────
- const testStringHello = "hello"
- var testResource = esv1.CRDProviderResource{
- Group: "example.io", Version: "v1alpha1", Kind: "Widget",
- }
- func makeStore(rules ...esv1.CRDProviderWhitelistRule) *esv1.CRDProvider {
- s := &esv1.CRDProvider{
- Auth: &esv1.KubernetesAuth{
- ServiceAccount: &esmeta.ServiceAccountSelector{Name: "reader"},
- },
- Resource: testResource,
- }
- if len(rules) > 0 {
- s.Whitelist = &esv1.CRDProviderWhitelist{Rules: rules}
- }
- return s
- }
- func wlRule(name string, props ...string) esv1.CRDProviderWhitelistRule {
- return esv1.CRDProviderWhitelistRule{Name: name, Properties: props}
- }
- func wlRuleNS(ns, name string, props ...string) esv1.CRDProviderWhitelistRule {
- return esv1.CRDProviderWhitelistRule{Namespace: ns, Name: name, Properties: props}
- }
- func widget(name, namespace string, spec map[string]any) *unstructured.Unstructured {
- metaData := map[string]any{"name": name}
- if namespace != "" {
- metaData["namespace"] = namespace
- }
- return &unstructured.Unstructured{Object: map[string]any{
- "apiVersion": "example.io/v1alpha1", "kind": "Widget",
- "metadata": metaData, "spec": spec,
- }}
- }
- // testWidgetGVK is the GroupVersionKind of the Widget test resource.
- var testWidgetGVK = schema.GroupVersionKind{Group: "example.io", Version: "v1alpha1", Kind: "Widget"}
- // fakeCRDClient builds a controller-runtime fake client that serves the Widget
- // test resource as unstructured objects, with a RESTMapper carrying the given
- // scope so List/Get behave like the production controller-runtime client.
- func fakeCRDClient(namespaced bool, objs ...kclient.Object) kclient.Client {
- scheme := runtime.NewScheme()
- scheme.AddKnownTypeWithName(testWidgetGVK, &unstructured.Unstructured{})
- scheme.AddKnownTypeWithName(testWidgetGVK.GroupVersion().WithKind(testWidgetGVK.Kind+"List"), &unstructured.UnstructuredList{})
- scope := meta.RESTScopeNamespace
- if !namespaced {
- scope = meta.RESTScopeRoot
- }
- mapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{testWidgetGVK.GroupVersion()})
- mapper.Add(testWidgetGVK, scope)
- return crfake.NewClientBuilder().WithScheme(scheme).WithRESTMapper(mapper).WithObjects(objs...).Build()
- }
- // newTestClient builds a Client for use in unit tests.
- // storeKind must be esv1.SecretStoreKind or esv1.ClusterSecretStoreKind.
- // Whitelist regexes must be valid: invalid patterns are caught at admission
- // (ValidateStore) and are not reachable via the Client constructor in production.
- func newTestClient(store *esv1.CRDProvider, storeKind, namespace string, namespaced bool, objs ...kclient.Object) *Client {
- rules, err := compileWhitelistRules(store.Whitelist)
- if err != nil {
- panic("newTestClient: invalid whitelist in test fixture: " + err.Error())
- }
- return &Client{
- store: store,
- namespace: namespace,
- namespaced: namespaced,
- storeKind: storeKind,
- kube: fakeCRDClient(namespaced, objs...),
- whitelistRules: rules,
- }
- }
- // Shorthands for the two most common configurations.
- func ssClient(store *esv1.CRDProvider, ns string, objs ...kclient.Object) *Client {
- return newTestClient(store, esv1.SecretStoreKind, ns, true, objs...)
- }
- func cssClient(store *esv1.CRDProvider, objs ...kclient.Object) *Client {
- return newTestClient(store, esv1.ClusterSecretStoreKind, "", true, objs...)
- }
- func ref(key, prop string) esv1.ExternalSecretDataRemoteRef {
- return esv1.ExternalSecretDataRemoteRef{Key: key, Property: prop}
- }
- // assertJSON unmarshals b and calls check on the result.
- func assertJSON[T any](t *testing.T, b []byte, check func(*testing.T, T)) {
- t.Helper()
- var v T
- if err := json.Unmarshal(b, &v); err != nil {
- t.Fatalf("unmarshal: %v\nraw: %s", err, b)
- }
- check(t, v)
- }
- // ── tests ────────────────────────────────────────────────────────────────────
- func TestClientBuildGVK(t *testing.T) {
- c := newTestClient(makeStore(), esv1.SecretStoreKind, "", true)
- gvk := c.buildGVK()
- if gvk.Group != "example.io" || gvk.Version != "v1alpha1" || gvk.Kind != "Widget" {
- t.Fatalf("unexpected GVK: %+v", gvk)
- }
- }
- func TestClientGetSecret(t *testing.T) {
- richSpec := map[string]any{
- "password": "pw1",
- "foo": map[string]any{"bar": int64(42), "baz": testStringHello},
- "nested": []any{map[string]any{"key": "ep", "val": "db:5432"}, map[string]any{"key": "fqdn", "val": "u:p@db"}},
- }
- tests := []struct {
- name string
- client func() *Client
- ref esv1.ExternalSecretDataRemoteRef
- wantStr string
- wantErrIs error
- wantErrMsg string
- checkFn func(*testing.T, []byte)
- }{
- // ── SecretStore ──
- {
- name: "empty key", client: func() *Client { return ssClient(makeStore(), "ns1", widget("x", "ns1", richSpec)) },
- ref: ref("", ""), wantErrMsg: "must not be empty",
- },
- {
- name: "slash rejected", client: func() *Client { return ssClient(makeStore(), "ns1", widget("x", "ns1", richSpec)) },
- ref: ref("a/b", ""), wantErrMsg: "must not contain '/'",
- },
- {
- name: "missing object", client: func() *Client { return ssClient(makeStore(), "ns1", widget("x", "ns1", richSpec)) },
- ref: ref("does-not-exist", ""), wantErrIs: esv1.NoSecretError{},
- },
- {
- name: "scalar property", client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
- ref: ref("item-a", "spec.password"), wantStr: "pw1",
- },
- {
- name: "nested scalar via dot path", client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
- ref: ref("item-a", "spec.foo.bar"), wantStr: "42",
- },
- {
- name: "gjson query on array", client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
- ref: ref("item-a", `spec.nested.#(key=="fqdn").val`), wantStr: "u:p@db",
- },
- {
- name: "nested object returns JSON",
- client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
- ref: ref("item-a", "spec.foo"),
- checkFn: func(t *testing.T, b []byte) {
- assertJSON(t, b, func(t *testing.T, m map[string]any) {
- if m["bar"] != float64(42) || m["baz"] != testStringHello {
- t.Fatalf("spec.foo = %v", m)
- }
- })
- },
- },
- {
- name: "array property returns JSON array",
- client: func() *Client { return ssClient(makeStore(), "ns1", widget("item-a", "ns1", richSpec)) },
- ref: ref("item-a", "spec.nested"),
- checkFn: func(t *testing.T, b []byte) {
- assertJSON(t, b, func(t *testing.T, arr []map[string]any) {
- if len(arr) != 2 || arr[0]["key"] != "ep" {
- t.Fatalf("spec.nested = %v", arr)
- }
- })
- },
- },
- // ── ClusterSecretStore: namespaced kind ──
- {
- name: "CSS: namespace/name resolves", client: func() *Client { return cssClient(makeStore(), widget("item-a", "ns1", richSpec)) },
- ref: ref("ns1/item-a", "spec.password"), wantStr: "pw1",
- },
- {
- name: "CSS: bare name rejected for namespaced kind", client: func() *Client { return cssClient(makeStore(), widget("item-a", "ns1", richSpec)) },
- ref: ref("item-a", ""), wantErrMsg: "namespace/objectName",
- },
- // ── ClusterSecretStore: cluster-scoped kind ──
- {
- name: "CSS cluster-scoped: bare name resolves",
- client: func() *Client {
- return newTestClient(makeStore(), esv1.ClusterSecretStoreKind, "default", false,
- widget("global", "", map[string]any{"password": "x"}))
- },
- ref: ref("global", "spec.password"), wantStr: "x",
- },
- {
- name: "CSS cluster-scoped: slash rejected",
- client: func() *Client {
- return newTestClient(makeStore(), esv1.ClusterSecretStoreKind, "default", false,
- widget("global", "", map[string]any{"password": "x"}))
- },
- ref: ref("ns/global", "spec.password"), wantErrMsg: "does not allow '/'",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := tt.client().GetSecret(context.Background(), tt.ref)
- switch {
- case tt.wantErrMsg != "":
- if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
- t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
- }
- case tt.wantErrIs != nil:
- if !errors.Is(err, tt.wantErrIs) {
- t.Fatalf("error = %v, want %T", err, tt.wantErrIs)
- }
- default:
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if tt.wantStr != "" && string(got) != tt.wantStr {
- t.Fatalf("= %q, want %q", string(got), tt.wantStr)
- }
- if tt.checkFn != nil {
- tt.checkFn(t, got)
- }
- }
- })
- }
- }
- func TestExtractValue(t *testing.T) {
- obj := widget("sample", "default", map[string]any{
- "password": "s3cr3t",
- "meta": map[string]any{"a": "b"},
- "targets": []any{map[string]any{"name": "app", "value": "v1"}, map[string]any{"name": "db", "value": "v2"}},
- })
- tests := []struct {
- name string
- property string
- fields []string
- wantStr string
- wantErrMsg string
- checkFn func(*testing.T, []byte)
- }{
- {name: "by property", property: "spec.password", wantStr: "s3cr3t"},
- {name: "missing property", property: "spec.missing", wantErrMsg: "not found"},
- {name: "query with no match is not found", property: `spec.targets.#(name=="nope").value`, wantErrMsg: "not found"},
- {name: "gjson array query", property: `spec.targets.#(name=="db").value`, wantStr: "v2"},
- {
- name: "selected fields", fields: []string{"spec.password", "spec.meta.a"},
- checkFn: func(t *testing.T, b []byte) {
- assertJSON(t, b, func(t *testing.T, m map[string]any) {
- if m["spec.password"] != "s3cr3t" || m["spec.meta.a"] != "b" {
- t.Fatalf("subset = %v", m)
- }
- })
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := extractValue(obj, tt.property, tt.fields)
- if tt.wantErrMsg != "" {
- if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
- t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
- }
- return
- }
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if tt.wantStr != "" && string(got) != tt.wantStr {
- t.Fatalf("= %q, want %q", string(got), tt.wantStr)
- }
- if tt.checkFn != nil {
- tt.checkFn(t, got)
- }
- })
- }
- }
- func TestJSONBytesToMap(t *testing.T) {
- tests := []struct {
- name string
- raw string
- checkFn func(*testing.T, map[string][]byte)
- }{
- {
- name: "mixed value types",
- raw: `{"a":"x","b":1}`,
- checkFn: func(t *testing.T, got map[string][]byte) {
- if string(got["a"]) != "x" || string(got["b"]) != "1" {
- t.Fatalf("got %v", got)
- }
- },
- },
- {
- name: "non-object falls back to value key",
- raw: `"hello"`,
- checkFn: func(t *testing.T, got map[string][]byte) {
- if string(got["value"]) != `"hello"` {
- t.Fatalf(`["value"] = %q`, string(got["value"]))
- }
- },
- },
- {
- name: "nested object preserved as JSON",
- raw: `{"user":"admin","foo":{"bar":42,"baz":"hello"}}`,
- checkFn: func(t *testing.T, got map[string][]byte) {
- if string(got["user"]) != "admin" {
- t.Fatalf(`["user"] = %q`, string(got["user"]))
- }
- assertJSON(t, got["foo"], func(t *testing.T, m map[string]any) {
- if m["bar"] != float64(42) || m["baz"] != testStringHello {
- t.Fatalf("foo = %v", m)
- }
- })
- },
- },
- {
- name: "array preserved as JSON",
- raw: `{"items":[{"key":"a","val":"1"},{"key":"b","val":"2"}]}`,
- checkFn: func(t *testing.T, got map[string][]byte) {
- assertJSON(t, got["items"], func(t *testing.T, items []map[string]any) {
- if len(items) != 2 || items[0]["key"] != "a" {
- t.Fatalf("items = %v", items)
- }
- })
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := jsonBytesToMap([]byte(tt.raw))
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- tt.checkFn(t, got)
- })
- }
- }
- func TestClientGetSecretMap(t *testing.T) {
- obj := widget("item-a", "ns1", map[string]any{
- "map": map[string]any{"a": "x", "b": int64(1)},
- "foo": map[string]any{"bar": int64(42), "baz": testStringHello},
- "nested": []any{map[string]any{"key": "ep", "val": "db:5432"}, map[string]any{"key": "fqdn", "val": "u:p@db"}},
- })
- c := ssClient(makeStore(), "ns1", obj)
- tests := []struct {
- name string
- ref esv1.ExternalSecretDataRemoteRef
- checkFn func(*testing.T, map[string][]byte)
- }{
- {
- name: "flat sub-object",
- ref: ref("item-a", "spec.map"),
- checkFn: func(t *testing.T, got map[string][]byte) {
- if string(got["a"]) != "x" || string(got["b"]) != "1" {
- t.Fatalf("got %v", got)
- }
- },
- },
- {
- name: "spec returns nested objects as JSON",
- ref: ref("item-a", "spec"),
- checkFn: func(t *testing.T, got map[string][]byte) {
- assertJSON(t, got["foo"], func(t *testing.T, m map[string]any) {
- if m["bar"] != float64(42) || m["baz"] != testStringHello {
- t.Fatalf("foo = %v", m)
- }
- })
- assertJSON(t, got["nested"], func(t *testing.T, arr []map[string]any) {
- if len(arr) != 2 || arr[0]["key"] != "ep" {
- t.Fatalf("nested = %v", arr)
- }
- })
- },
- },
- {
- name: "spec.foo returns flat map",
- ref: ref("item-a", "spec.foo"),
- checkFn: func(t *testing.T, got map[string][]byte) {
- if string(got["bar"]) != "42" || string(got["baz"]) != testStringHello {
- t.Fatalf("got %v", got)
- }
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := c.GetSecretMap(context.Background(), tt.ref)
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- tt.checkFn(t, got)
- })
- }
- }
- func TestClientGetAllSecrets(t *testing.T) {
- objA := widget("app-a", "ns1", map[string]any{"password": "a"})
- objB := widget("sys-b", "ns1", map[string]any{"password": "b"})
- tests := []struct {
- name string
- client func() *Client
- find esv1.ExternalSecretFind
- wantKeys []string
- wantErrMsg string
- }{
- {
- name: "no filter returns all", wantKeys: []string{"app-a", "sys-b"},
- client: func() *Client { return ssClient(makeStore(), "ns1", objA, objB) },
- },
- {
- name: "regexp filters list", wantKeys: []string{"sys-b"},
- client: func() *Client { return ssClient(makeStore(), "ns1", objA, objB) },
- find: esv1.ExternalSecretFind{Name: &esv1.FindName{RegExp: "^sys-.*$"}},
- },
- {
- name: "invalid regex", wantErrMsg: "invalid name pattern",
- client: func() *Client { return ssClient(makeStore(), "ns1", objA) },
- find: esv1.ExternalSecretFind{Name: &esv1.FindName{RegExp: "("}},
- },
- {
- name: "whitelist name rule", wantKeys: []string{"app-a"},
- client: func() *Client { return ssClient(makeStore(wlRule("^app-.*$")), "ns1", objA, objB) },
- },
- {
- // The provider builds namespace/name keys; the conversion strategy
- // then replaces the slash so the key is valid in a Secret.
- name: "CSS namespaced kind uses namespace/name keys", wantKeys: []string{"ns1_app-a", "ns2_sys-b"},
- client: func() *Client {
- return cssClient(makeStore(),
- widget("app-a", "ns1", map[string]any{"password": "a"}),
- widget("sys-b", "ns2", map[string]any{"password": "b"}))
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := tt.client().GetAllSecrets(context.Background(), tt.find)
- if tt.wantErrMsg != "" {
- if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
- t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
- }
- return
- }
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(got) != len(tt.wantKeys) {
- t.Fatalf("len = %d, want %d; keys: %v", len(got), len(tt.wantKeys), got)
- }
- for _, k := range tt.wantKeys {
- if _, ok := got[k]; !ok {
- t.Fatalf("missing key %q", k)
- }
- }
- })
- }
- }
- func TestClientMiscMethods(t *testing.T) {
- c := ssClient(makeStore(), "ns1")
- if err := c.PushSecret(context.Background(), nil, testPushSecretData{}); err == nil {
- t.Fatal("PushSecret() expected error")
- }
- if err := c.DeleteSecret(context.Background(), testPushSecretRemoteRef{}); err == nil {
- t.Fatal("DeleteSecret() expected error")
- }
- if got, err := c.Validate(); err != nil || got != esv1.ValidationResultReady {
- t.Fatalf("Validate() = (%v, %v), want (%v, nil)", got, err, esv1.ValidationResultReady)
- }
- if err := c.Close(context.Background()); err != nil {
- t.Fatalf("Close() unexpected error: %v", err)
- }
- }
- func TestReadsRejectReferentStub(t *testing.T) {
- // The referent stub (returned by newClient for a ClusterSecretStore whose SA
- // namespace is not yet known) has no kube client. Every read must return
- // errClientNotReady instead of nil-panicking on c.kube.
- c := &Client{referent: true, storeKind: esv1.ClusterSecretStoreKind}
- ctx := context.Background()
- if _, err := c.GetSecret(ctx, esv1.ExternalSecretDataRemoteRef{Key: "widget"}); !errors.Is(err, errClientNotReady) {
- t.Fatalf("GetSecret() err = %v, want errClientNotReady", err)
- }
- if _, err := c.GetSecretMap(ctx, esv1.ExternalSecretDataRemoteRef{Key: "widget"}); !errors.Is(err, errClientNotReady) {
- t.Fatalf("GetSecretMap() err = %v, want errClientNotReady", err)
- }
- if _, err := c.GetAllSecrets(ctx, esv1.ExternalSecretFind{}); !errors.Is(err, errClientNotReady) {
- t.Fatalf("GetAllSecrets() err = %v, want errClientNotReady", err)
- }
- if _, err := c.SecretExists(ctx, testPushSecretRemoteRef{remoteKey: "widget"}); !errors.Is(err, errClientNotReady) {
- t.Fatalf("SecretExists() err = %v, want errClientNotReady", err)
- }
- }
- func TestClientSecretExists(t *testing.T) {
- obj := widget("item-a", "ns1", map[string]any{"password": "pw1"})
- c := ssClient(makeStore(), "ns1", obj)
- if exists, err := c.SecretExists(context.Background(), testPushSecretRemoteRef{remoteKey: "item-a"}); err != nil || !exists {
- t.Fatalf("SecretExists(item-a) = (%v, %v), want (true, nil)", exists, err)
- }
- if exists, err := c.SecretExists(context.Background(), testPushSecretRemoteRef{remoteKey: "missing"}); err != nil || exists {
- t.Fatalf("SecretExists(missing) = (%v, %v), want (false, nil)", exists, err)
- }
- }
- // TestWhitelistMatching covers all whitelist filter dimensions: name, namespace,
- // properties, and combinations — as a single table-driven test.
- func TestWhitelistMatching(t *testing.T) {
- obj := widget("item-a", "ns1", map[string]any{"password": "pw1"})
- tests := []struct {
- name string
- client func() *Client
- ref esv1.ExternalSecretDataRemoteRef
- wantVal string
- wantErrMsg string
- }{
- // ── name-only rules (SecretStore) ──
- {
- name: "denied when no rule matches", wantErrMsg: "denied by whitelist",
- client: func() *Client { return ssClient(makeStore(wlRule("^allowed-.*$")), "ns1", obj) },
- ref: ref("item-a", "spec.password"),
- },
- {
- name: "allowed by name rule", wantVal: "pw1",
- client: func() *Client { return ssClient(makeStore(wlRule("^item-.*$")), "ns1", obj) },
- ref: ref("item-a", "spec.password"),
- },
- // ── name + properties ──
- {
- name: "denied when property does not match", wantErrMsg: "denied by whitelist",
- client: func() *Client { return ssClient(makeStore(wlRule("^item-.*$", `^spec\.allowed$`)), "ns1", obj) },
- ref: ref("item-a", "spec.password"),
- },
- {
- name: "allowed when both name and property match", wantVal: "pw1",
- client: func() *Client { return ssClient(makeStore(wlRule("^item-.*$", `^spec\.password$`)), "ns1", obj) },
- ref: ref("item-a", "spec.password"),
- },
- // ── properties-only ──
- {
- name: "allowed when one of two properties matches", wantVal: "pw1",
- client: func() *Client {
- return ssClient(makeStore(esv1.CRDProviderWhitelistRule{Properties: []string{`^spec\.username$`, `^spec\.password$`}}), "ns1", obj)
- },
- ref: ref("item-a", "spec.password"),
- },
- {
- name: "denied when no property matches", wantErrMsg: "denied by whitelist",
- client: func() *Client {
- return ssClient(makeStore(esv1.CRDProviderWhitelistRule{Properties: []string{`^spec\.username$`, `^spec\.token$`}}), "ns1", obj)
- },
- ref: ref("item-a", "spec.password"),
- },
- // ── namespace rules (ClusterSecretStore) ──
- {
- name: "CSS: namespace allows matching NS", wantVal: "pw1",
- client: func() *Client { return cssClient(makeStore(wlRuleNS("^ns1$", "")), obj) },
- ref: ref("ns1/item-a", "spec.password"),
- },
- {
- name: "CSS: namespace denies non-matching NS", wantErrMsg: "denied by whitelist",
- client: func() *Client { return cssClient(makeStore(wlRuleNS("^prod$", "")), obj) },
- ref: ref("ns1/item-a", "spec.password"),
- },
- {
- name: "CSS: namespace regex pattern", wantVal: "pw1",
- client: func() *Client { return cssClient(makeStore(wlRuleNS("^ns.*$", "")), obj) },
- ref: ref("ns1/item-a", "spec.password"),
- },
- {
- name: "CSS: namespace + name both must match", wantErrMsg: "denied by whitelist",
- client: func() *Client { return cssClient(makeStore(wlRuleNS("^ns1$", "^other-.*$")), obj) },
- ref: ref("ns1/item-a", "spec.password"),
- },
- {
- name: "SecretStore ignores namespace rule", wantVal: "pw1",
- client: func() *Client { return ssClient(makeStore(wlRuleNS("^prod$", "")), "ns1", obj) },
- ref: ref("item-a", "spec.password"),
- },
- {
- // Regression: namespace rule must not match cluster-scoped objects.
- name: "CSS: namespace rule does not match cluster-scoped object", wantErrMsg: "denied by whitelist",
- client: func() *Client {
- return newTestClient(makeStore(wlRuleNS("^prod$", "")), esv1.ClusterSecretStoreKind, "", false,
- widget("item-a", "", map[string]any{"password": "pw1"}))
- },
- ref: ref("item-a", "spec.password"),
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, err := tt.client().GetSecret(context.Background(), tt.ref)
- if tt.wantErrMsg != "" {
- if err == nil || !strings.Contains(err.Error(), tt.wantErrMsg) {
- t.Fatalf("error = %v, want %q", err, tt.wantErrMsg)
- }
- return
- }
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if string(got) != tt.wantVal {
- t.Fatalf("= %q, want %q", string(got), tt.wantVal)
- }
- })
- }
- }
- // TestWhitelistGetAllSecrets verifies namespace whitelist filtering in GetAllSecrets.
- func TestWhitelistGetAllSecrets(t *testing.T) {
- o1 := widget("app-a", "ns1", map[string]any{"password": "a"})
- o2 := widget("app-b", "ns2", map[string]any{"password": "b"})
- tests := []struct {
- name string
- rules []esv1.CRDProviderWhitelistRule
- wantKeys []string
- }{
- {name: "allow only ns1", rules: []esv1.CRDProviderWhitelistRule{wlRuleNS("^ns1$", "")}, wantKeys: []string{"ns1_app-a"}},
- {name: "ns1 + name rule", rules: []esv1.CRDProviderWhitelistRule{wlRuleNS("^ns1$", ""), wlRuleNS("", "^app-b$")}, wantKeys: []string{"ns1_app-a", "ns2_app-b"}},
- {name: "filter to ns2", rules: []esv1.CRDProviderWhitelistRule{wlRuleNS("^ns2$", "")}, wantKeys: []string{"ns2_app-b"}},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- c := cssClient(makeStore(tt.rules...), o1, o2)
- got, err := c.GetAllSecrets(context.Background(), esv1.ExternalSecretFind{})
- if err != nil {
- t.Fatalf("unexpected error: %v", err)
- }
- if len(got) != len(tt.wantKeys) {
- t.Fatalf("len = %d, want %d; keys: %v", len(got), len(tt.wantKeys), got)
- }
- for _, k := range tt.wantKeys {
- if _, ok := got[k]; !ok {
- t.Fatalf("missing key %q; got %v", k, got)
- }
- }
- })
- }
- }
|