|
|
@@ -33,6 +33,7 @@ import (
|
|
|
|
|
|
v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
|
|
|
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
|
|
|
+ esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
|
|
|
)
|
|
|
|
|
|
func TestProviderGetSecret(t *testing.T) {
|
|
|
@@ -103,8 +104,8 @@ func TestProviderGetSecret(t *testing.T) {
|
|
|
for _, tt := range tests {
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- client: tt.client(),
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
+ client: tt.client(),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
}
|
|
|
got, err := p.GetSecret(t.Context(), tt.ref)
|
|
|
tt.assertError(t, err)
|
|
|
@@ -273,8 +274,8 @@ func TestProviderGetSecretMap(t *testing.T) {
|
|
|
for _, tt := range tests {
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- client: tt.client(),
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
+ client: tt.client(),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
}
|
|
|
got, err := p.GetSecretMap(t.Context(), tt.ref)
|
|
|
tt.assertError(t, err)
|
|
|
@@ -285,11 +286,11 @@ func TestProviderGetSecretMap(t *testing.T) {
|
|
|
|
|
|
func TestProviderValidate(t *testing.T) {
|
|
|
tests := []struct {
|
|
|
- name string
|
|
|
- want v1.ValidationResult
|
|
|
- assertError func(t *testing.T, err error)
|
|
|
- client func() *onepassword.Client
|
|
|
- vaultPrefix string
|
|
|
+ name string
|
|
|
+ want v1.ValidationResult
|
|
|
+ assertError func(t *testing.T, err error)
|
|
|
+ client func() *onepassword.Client
|
|
|
+ targetPrefix string
|
|
|
}{
|
|
|
{
|
|
|
name: "validate successfully",
|
|
|
@@ -312,14 +313,14 @@ func TestProviderValidate(t *testing.T) {
|
|
|
assertError: func(t *testing.T, err error) {
|
|
|
require.NoError(t, err)
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
},
|
|
|
}
|
|
|
for _, tt := range tests {
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- client: tt.client(),
|
|
|
- vaultPrefix: tt.vaultPrefix,
|
|
|
+ client: tt.client(),
|
|
|
+ targetPrefix: tt.targetPrefix,
|
|
|
}
|
|
|
got, err := p.Validate()
|
|
|
tt.assertError(t, err)
|
|
|
@@ -642,6 +643,20 @@ func (f *fakeLister) Files() onepassword.ItemsFilesAPI {
|
|
|
return f.fileLister
|
|
|
}
|
|
|
|
|
|
+func (f *fakeLister) CreateAll(ctx context.Context, vaultID string, params []onepassword.ItemCreateParams) (onepassword.ItemsUpdateAllResponse, error) {
|
|
|
+ return onepassword.ItemsUpdateAllResponse{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeLister) GetAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsGetAllResponse, error) {
|
|
|
+ return onepassword.ItemsGetAllResponse{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeLister) DeleteAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsDeleteAllResponse, error) {
|
|
|
+ return onepassword.ItemsDeleteAllResponse{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+var _ onepassword.ItemsAPI = (*fakeLister)(nil)
|
|
|
+
|
|
|
type fakeFileLister struct {
|
|
|
readContent []byte
|
|
|
}
|
|
|
@@ -725,6 +740,18 @@ func (f *statefulFakeLister) Files() onepassword.ItemsFilesAPI {
|
|
|
return f.fileLister
|
|
|
}
|
|
|
|
|
|
+func (f *statefulFakeLister) CreateAll(ctx context.Context, vaultID string, params []onepassword.ItemCreateParams) (onepassword.ItemsUpdateAllResponse, error) {
|
|
|
+ return onepassword.ItemsUpdateAllResponse{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *statefulFakeLister) GetAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsGetAllResponse, error) {
|
|
|
+ return onepassword.ItemsGetAllResponse{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *statefulFakeLister) DeleteAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsDeleteAllResponse, error) {
|
|
|
+ return onepassword.ItemsDeleteAllResponse{}, nil
|
|
|
+}
|
|
|
+
|
|
|
var _ onepassword.ItemsAPI = (*statefulFakeLister)(nil)
|
|
|
|
|
|
type fakeClient struct {
|
|
|
@@ -734,12 +761,48 @@ type fakeClient struct {
|
|
|
resolveAllError error
|
|
|
listAllResult []onepassword.VaultOverview
|
|
|
listAllError error
|
|
|
+
|
|
|
+ envVariables []onepassword.EnvironmentVariable
|
|
|
+ envError error
|
|
|
+ envGetVariablesCalls int
|
|
|
}
|
|
|
|
|
|
-func (f *fakeClient) List(ctx context.Context) ([]onepassword.VaultOverview, error) {
|
|
|
+func (f *fakeClient) List(ctx context.Context, params ...onepassword.VaultListParams) ([]onepassword.VaultOverview, error) {
|
|
|
return f.listAllResult, f.listAllError
|
|
|
}
|
|
|
|
|
|
+func (f *fakeClient) Create(ctx context.Context, params onepassword.VaultCreateParams) (onepassword.Vault, error) {
|
|
|
+ return onepassword.Vault{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) Get(ctx context.Context, vaultID string, params onepassword.VaultGetParams) (onepassword.Vault, error) {
|
|
|
+ return onepassword.Vault{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) GetOverview(ctx context.Context, vaultID string) (onepassword.VaultOverview, error) {
|
|
|
+ return onepassword.VaultOverview{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) Update(ctx context.Context, vaultID string, params onepassword.VaultUpdateParams) (onepassword.Vault, error) {
|
|
|
+ return onepassword.Vault{}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) Delete(ctx context.Context, vaultID string) error {
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) GrantGroupPermissions(ctx context.Context, vaultID string, groupPermissionsList []onepassword.GroupAccess) error {
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) UpdateGroupPermissions(ctx context.Context, groupPermissionsList []onepassword.GroupVaultAccess) error {
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakeClient) RevokeGroupPermissions(ctx context.Context, vaultID, groupID string) error {
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
func (f *fakeClient) Resolve(ctx context.Context, secretReference string) (string, error) {
|
|
|
return f.resolveResult, f.resolveError
|
|
|
}
|
|
|
@@ -748,6 +811,20 @@ func (f *fakeClient) ResolveAll(ctx context.Context, secretReferences []string)
|
|
|
return f.resolveAll, f.resolveAllError
|
|
|
}
|
|
|
|
|
|
+func (f *fakeClient) GetVariables(ctx context.Context, environmentID string) (onepassword.GetVariablesResponse, error) {
|
|
|
+ f.envGetVariablesCalls++
|
|
|
+ if f.envError != nil {
|
|
|
+ return onepassword.GetVariablesResponse{}, f.envError
|
|
|
+ }
|
|
|
+ return onepassword.GetVariablesResponse{Variables: f.envVariables}, nil
|
|
|
+}
|
|
|
+
|
|
|
+var (
|
|
|
+ _ onepassword.VaultsAPI = (*fakeClient)(nil)
|
|
|
+ _ onepassword.SecretsAPI = (*fakeClient)(nil)
|
|
|
+ _ onepassword.EnvironmentsAPI = (*fakeClient)(nil)
|
|
|
+)
|
|
|
+
|
|
|
func TestDeleteMultipleFieldsFromSameItem(t *testing.T) {
|
|
|
fc := &fakeClient{
|
|
|
listAllResult: []onepassword.VaultOverview{
|
|
|
@@ -842,7 +919,7 @@ func TestCachingGetSecret(t *testing.T) {
|
|
|
SecretsAPI: fcWithCounter,
|
|
|
VaultsAPI: fcWithCounter.fakeClient,
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
}
|
|
|
|
|
|
// Initialize cache
|
|
|
@@ -875,8 +952,8 @@ func TestCachingGetSecret(t *testing.T) {
|
|
|
SecretsAPI: fcWithCounter,
|
|
|
VaultsAPI: fcWithCounter.fakeClient,
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- cache: nil, // Cache disabled
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ cache: nil, // Cache disabled
|
|
|
}
|
|
|
|
|
|
ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
|
|
|
@@ -924,9 +1001,9 @@ func TestCachingGetSecretMap(t *testing.T) {
|
|
|
VaultsAPI: fc,
|
|
|
ItemsAPI: flWithCounter,
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- vaultID: "vault-id",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ targetID: "vault-id",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
|
|
|
ref := v1.ExternalSecretDataRemoteRef{Key: "item"}
|
|
|
@@ -974,9 +1051,9 @@ func TestCacheInvalidationPushSecret(t *testing.T) {
|
|
|
VaultsAPI: fcWithCounter.fakeClient,
|
|
|
ItemsAPI: fl,
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- vaultID: "vault-id",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ targetID: "vault-id",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
|
|
|
ref := v1.ExternalSecretDataRemoteRef{Key: "item/password"}
|
|
|
@@ -1044,9 +1121,9 @@ func TestCacheInvalidationStaleItemAfterPush(t *testing.T) {
|
|
|
VaultsAPI: fc,
|
|
|
ItemsAPI: fl,
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- vaultID: "vault-id",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ targetID: "vault-id",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
|
|
|
mapRef := v1.ExternalSecretDataRemoteRef{Key: "key", Property: "password"}
|
|
|
@@ -1106,9 +1183,9 @@ func TestCacheInvalidationDeleteSecret(t *testing.T) {
|
|
|
VaultsAPI: fcWithCounter.fakeClient,
|
|
|
ItemsAPI: fl,
|
|
|
},
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- vaultID: "vault-id",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ targetID: "vault-id",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
|
|
|
ref := v1.ExternalSecretDataRemoteRef{Key: "item/field1"}
|
|
|
@@ -1136,8 +1213,8 @@ func TestCacheInvalidationDeleteSecret(t *testing.T) {
|
|
|
func TestInvalidateCacheByPrefix(t *testing.T) {
|
|
|
t.Run("invalidates all entries with prefix", func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
|
|
|
// Add multiple cache entries
|
|
|
@@ -1162,8 +1239,8 @@ func TestInvalidateCacheByPrefix(t *testing.T) {
|
|
|
|
|
|
t.Run("handles nil cache gracefully", func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- cache: nil,
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ cache: nil,
|
|
|
}
|
|
|
|
|
|
// Should not panic
|
|
|
@@ -1172,8 +1249,8 @@ func TestInvalidateCacheByPrefix(t *testing.T) {
|
|
|
|
|
|
t.Run("does not invalidate entries with similar prefixes", func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
|
|
|
p.cache.Add("op://vault/item/field1", []byte("val1"))
|
|
|
@@ -1410,7 +1487,7 @@ func TestSecretExists(t *testing.T) {
|
|
|
VaultsAPI: fc,
|
|
|
ItemsAPI: tt.lister,
|
|
|
},
|
|
|
- vaultID: "vault-id",
|
|
|
+ targetID: "vault-id",
|
|
|
}
|
|
|
exists, err := p.SecretExists(t.Context(), tt.ref)
|
|
|
tt.assertError(t, err)
|
|
|
@@ -1491,7 +1568,7 @@ func TestPushSecretFieldType(t *testing.T) {
|
|
|
VaultsAPI: fc,
|
|
|
ItemsAPI: fl,
|
|
|
},
|
|
|
- vaultID: "vault-id",
|
|
|
+ targetID: "vault-id",
|
|
|
}
|
|
|
|
|
|
ref := v1alpha1.PushSecretData{
|
|
|
@@ -1884,8 +1961,8 @@ func TestGetAllSecrets(t *testing.T) {
|
|
|
for _, tt := range tests {
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
p := &SecretsClient{
|
|
|
- client: tt.client(),
|
|
|
- vaultPrefix: "op://vault/",
|
|
|
+ client: tt.client(),
|
|
|
+ targetPrefix: "op://vault/",
|
|
|
}
|
|
|
got, err := p.GetAllSecrets(t.Context(), tt.ref)
|
|
|
tt.assertError(t, err)
|
|
|
@@ -1964,9 +2041,9 @@ func TestCachingGetAllSecrets(t *testing.T) {
|
|
|
},
|
|
|
}
|
|
|
return &SecretsClient{
|
|
|
- client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl},
|
|
|
- vaultID: "vault-id",
|
|
|
- cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl},
|
|
|
+ targetID: "vault-id",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2110,7 +2187,7 @@ func TestPushAllKeys(t *testing.T) {
|
|
|
|
|
|
t.Run("creates new item with all secret keys as concealed fields", func(t *testing.T) {
|
|
|
fl := newLister()
|
|
|
- p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
|
|
|
+ p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
|
|
|
require.NoError(t, p.PushSecret(t.Context(), secret("alpha", "val-alpha", "beta", "val-beta"), ref("", "my-item")))
|
|
|
require.True(t, fl.createCalled)
|
|
|
assert.False(t, fl.putCalled)
|
|
|
@@ -2122,7 +2199,7 @@ func TestPushAllKeys(t *testing.T) {
|
|
|
|
|
|
t.Run("updates existing item with all secret keys", func(t *testing.T) {
|
|
|
fl := newLister(onepassword.Item{ID: "item-id", Title: testExistingItem, VaultID: "vault-id"})
|
|
|
- p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
|
|
|
+ p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
|
|
|
require.NoError(t, p.PushSecret(t.Context(), secret("key1", "value1", "key2", "value2"), ref("", testExistingItem)))
|
|
|
assert.False(t, fl.createCalled)
|
|
|
require.True(t, fl.putCalled)
|
|
|
@@ -2133,7 +2210,7 @@ func TestPushAllKeys(t *testing.T) {
|
|
|
|
|
|
t.Run("applies tags from metadata on create", func(t *testing.T) {
|
|
|
fl := newLister()
|
|
|
- p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
|
|
|
+ p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
|
|
|
meta := `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"tags":["env:prod","team:backend"]}}`
|
|
|
require.NoError(t, p.PushSecret(t.Context(), secret("k", "v"), ref("", "tagged-item", meta)))
|
|
|
require.True(t, fl.createCalled)
|
|
|
@@ -2142,7 +2219,7 @@ func TestPushAllKeys(t *testing.T) {
|
|
|
|
|
|
t.Run("removes fields deleted from the secret", func(t *testing.T) {
|
|
|
fl := newLister(existingItem) // existingItem has field testOldKey
|
|
|
- p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
|
|
|
+ p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
|
|
|
// secret no longer contains testOldKey, only "new-key"
|
|
|
require.NoError(t, p.PushSecret(t.Context(), secret("new-key", "new-val"), ref("", testExistingItem)))
|
|
|
require.True(t, fl.putCalled)
|
|
|
@@ -2152,3 +2229,233 @@ func TestPushAllKeys(t *testing.T) {
|
|
|
assert.False(t, stillThere, "deleted key must be removed from the 1Password item")
|
|
|
})
|
|
|
}
|
|
|
+
|
|
|
+func newEnvClient(fc *fakeClient) *SecretsClient {
|
|
|
+ return &SecretsClient{
|
|
|
+ client: &onepassword.Client{EnvironmentsAPI: fc, SecretsAPI: fc, VaultsAPI: fc},
|
|
|
+ source: sourceEnvironment,
|
|
|
+ targetID: "env-id",
|
|
|
+ targetPrefix: "op://env/env-id/",
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestEnvironmentGetSecret(t *testing.T) {
|
|
|
+ t.Run("returns variable value", func(t *testing.T) {
|
|
|
+ fc := &fakeClient{
|
|
|
+ envVariables: []onepassword.EnvironmentVariable{
|
|
|
+ {Name: "DB_PASSWORD", Value: "s3cret"},
|
|
|
+ {Name: "API_KEY", Value: "xyz"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+ p := newEnvClient(fc)
|
|
|
+
|
|
|
+ got, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "DB_PASSWORD"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, []byte("s3cret"), got)
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("returns ErrKeyNotFound when variable absent", func(t *testing.T) {
|
|
|
+ fc := &fakeClient{
|
|
|
+ envVariables: []onepassword.EnvironmentVariable{
|
|
|
+ {Name: "OTHER", Value: "v"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+ p := newEnvClient(fc)
|
|
|
+
|
|
|
+ _, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "MISSING"})
|
|
|
+ require.ErrorIs(t, err, ErrKeyNotFound)
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("propagates SDK errors", func(t *testing.T) {
|
|
|
+ fc := &fakeClient{envError: errors.New("upstream boom")}
|
|
|
+ p := newEnvClient(fc)
|
|
|
+
|
|
|
+ _, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "ANY"})
|
|
|
+ require.ErrorContains(t, err, "upstream boom")
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("rejects ref.Version", func(t *testing.T) {
|
|
|
+ p := newEnvClient(&fakeClient{})
|
|
|
+ _, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "K", Version: "1"})
|
|
|
+ require.ErrorContains(t, err, "is not implemented")
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("uses cache on subsequent reads", func(t *testing.T) {
|
|
|
+ fc := &fakeClient{
|
|
|
+ envVariables: []onepassword.EnvironmentVariable{
|
|
|
+ {Name: "K1", Value: "v1"},
|
|
|
+ {Name: "K2", Value: "v2"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+ p := newEnvClient(fc)
|
|
|
+ p.cache = expirable.NewLRU[string, []byte](100, nil, time.Minute)
|
|
|
+
|
|
|
+ _, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "K1"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ _, err = p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "K2"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, 1, fc.envGetVariablesCalls, "second lookup must hit the cached fetch, not the API")
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func TestEnvironmentGetSecretMap(t *testing.T) {
|
|
|
+ fc := &fakeClient{
|
|
|
+ envVariables: []onepassword.EnvironmentVariable{
|
|
|
+ {Name: "K1", Value: "v1"},
|
|
|
+ {Name: "K2", Value: "v2"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+ p := newEnvClient(fc)
|
|
|
+
|
|
|
+ t.Run("returns all variables when no property", func(t *testing.T) {
|
|
|
+ got, err := p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, map[string][]byte{"K1": []byte("v1"), "K2": []byte("v2")}, got)
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("filters by property when set", func(t *testing.T) {
|
|
|
+ got, err := p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{Property: "K2"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, map[string][]byte{"K2": []byte("v2")}, got)
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("returns ErrKeyNotFound when property missing", func(t *testing.T) {
|
|
|
+ _, err := p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{Property: "missing"})
|
|
|
+ require.ErrorIs(t, err, ErrKeyNotFound)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func TestEnvironmentGetAllSecrets(t *testing.T) {
|
|
|
+ fc := &fakeClient{
|
|
|
+ envVariables: []onepassword.EnvironmentVariable{
|
|
|
+ {Name: "K1", Value: "v1"},
|
|
|
+ {Name: "K2", Value: "v2"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+ p := newEnvClient(fc)
|
|
|
+
|
|
|
+ got, err := p.GetAllSecrets(t.Context(), v1.ExternalSecretFind{})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, map[string][]byte{"K1": []byte("v1"), "K2": []byte("v2")}, got)
|
|
|
+}
|
|
|
+
|
|
|
+func TestEnvironmentRejectsWrites(t *testing.T) {
|
|
|
+ p := newEnvClient(&fakeClient{})
|
|
|
+
|
|
|
+ t.Run("PushSecret", func(t *testing.T) {
|
|
|
+ err := p.PushSecret(t.Context(), &corev1.Secret{Data: map[string][]byte{"k": []byte("v")}}, &fakePushSecretData{secretKey: "k", remoteKey: "rk"})
|
|
|
+ require.ErrorContains(t, err, "read-only")
|
|
|
+ require.ErrorContains(t, err, "PushSecret")
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("DeleteSecret", func(t *testing.T) {
|
|
|
+ err := p.DeleteSecret(t.Context(), &fakePushSecretData{remoteKey: "rk"})
|
|
|
+ require.ErrorContains(t, err, "read-only")
|
|
|
+ require.ErrorContains(t, err, "DeleteSecret")
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("SecretExists", func(t *testing.T) {
|
|
|
+ _, err := p.SecretExists(t.Context(), &fakePushSecretData{remoteKey: "rk"})
|
|
|
+ require.ErrorContains(t, err, "read-only")
|
|
|
+ require.ErrorContains(t, err, "SecretExists")
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+type fakePushSecretData struct {
|
|
|
+ secretKey string
|
|
|
+ remoteKey string
|
|
|
+ property string
|
|
|
+ metadata *apiextensionsv1.JSON
|
|
|
+}
|
|
|
+
|
|
|
+func (f *fakePushSecretData) GetSecretKey() string { return f.secretKey }
|
|
|
+func (f *fakePushSecretData) GetRemoteKey() string { return f.remoteKey }
|
|
|
+func (f *fakePushSecretData) GetProperty() string { return f.property }
|
|
|
+func (f *fakePushSecretData) GetMetadata() *apiextensionsv1.JSON { return f.metadata }
|
|
|
+
|
|
|
+func TestEnvironmentVsVaultCacheIsolation(t *testing.T) {
|
|
|
+ fc := &fakeClient{
|
|
|
+ resolveResult: "from-vault",
|
|
|
+ envVariables: []onepassword.EnvironmentVariable{
|
|
|
+ {Name: "shared", Value: "from-env"},
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ vault := &SecretsClient{
|
|
|
+ client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, EnvironmentsAPI: fc},
|
|
|
+ source: sourceVault,
|
|
|
+ targetID: "vault-id",
|
|
|
+ targetPrefix: "op://myvault/",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ }
|
|
|
+ env := &SecretsClient{
|
|
|
+ client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, EnvironmentsAPI: fc},
|
|
|
+ source: sourceEnvironment,
|
|
|
+ targetID: "env-id",
|
|
|
+ targetPrefix: "op://env/env-id/",
|
|
|
+ cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
|
|
|
+ }
|
|
|
+
|
|
|
+ gotVault, err := vault.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "shared"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, []byte("from-vault"), gotVault)
|
|
|
+
|
|
|
+ gotEnv, err := env.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "shared"})
|
|
|
+ require.NoError(t, err)
|
|
|
+ require.Equal(t, []byte("from-env"), gotEnv, "vault and environment must not share cache entries for the same key")
|
|
|
+}
|
|
|
+
|
|
|
+func TestProviderValidateStore(t *testing.T) {
|
|
|
+ baseAuth := &v1.OnePasswordSDKAuth{
|
|
|
+ ServiceAccountSecretRef: esmeta.SecretKeySelector{
|
|
|
+ Name: "sa-secret",
|
|
|
+ Key: "token",
|
|
|
+ },
|
|
|
+ }
|
|
|
+ provider := &Provider{}
|
|
|
+
|
|
|
+ mkStore := func(p *v1.OnePasswordSDKProvider) *v1.SecretStore {
|
|
|
+ return &v1.SecretStore{
|
|
|
+ TypeMeta: metav1.TypeMeta{Kind: "SecretStore"},
|
|
|
+ Spec: v1.SecretStoreSpec{
|
|
|
+ Provider: &v1.SecretStoreProvider{OnePasswordSDK: p},
|
|
|
+ },
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ spec *v1.OnePasswordSDKProvider
|
|
|
+ wantErr string
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "vault only is valid",
|
|
|
+ spec: &v1.OnePasswordSDKProvider{Auth: baseAuth, Vault: "myvault"},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "environment only is valid",
|
|
|
+ spec: &v1.OnePasswordSDKProvider{Auth: baseAuth, Environment: "env-id"},
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "neither set is invalid",
|
|
|
+ spec: &v1.OnePasswordSDKProvider{Auth: baseAuth},
|
|
|
+ wantErr: "exactly one of",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "both set is invalid",
|
|
|
+ spec: &v1.OnePasswordSDKProvider{Auth: baseAuth, Vault: "v", Environment: "e"},
|
|
|
+ wantErr: "mutually exclusive",
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ t.Run(tt.name, func(t *testing.T) {
|
|
|
+ _, err := provider.ValidateStore(mkStore(tt.spec))
|
|
|
+ if tt.wantErr == "" {
|
|
|
+ require.NoError(t, err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ require.ErrorContains(t, err, tt.wantErr)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|