1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

fix: introducing support for conversion strategy for PushSecret. (#3292)

* fix: introducing support for conversion strategy for PushSecret.

Signed-off-by: Rodrigo Fior Kuntzer <rodrigo@miro.com>

* fix: unit tests code quality.

Signed-off-by: Rodrigo Fior Kuntzer <rodrigo@miro.com>

---------

Signed-off-by: Rodrigo Fior Kuntzer <rodrigo@miro.com>
This commit is contained in:
Rodrigo Fior Kuntzer 2024-04-04 16:31:28 +02:00 committed by GitHub
parent ac6d53da54
commit 9ff2354213
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 281 additions and 5 deletions

View file

@ -57,6 +57,14 @@ const (
PushSecretDeletionPolicyNone PushSecretDeletionPolicy = "None"
)
// +kubebuilder:validation:Enum=None;ReverseUnicode
type PushSecretConversionStrategy string
const (
PushSecretConversionNone PushSecretConversionStrategy = "None"
PushSecretConversionReverseUnicode PushSecretConversionStrategy = "ReverseUnicode"
)
// PushSecretSpec configures the behavior of the PushSecret.
type PushSecretSpec struct {
// The Interval to which External Secrets will try to push a secret definition
@ -121,6 +129,10 @@ type PushSecretData struct {
// The structure of metadata is provider specific, please look it up in the provider documentation.
// +optional
Metadata *apiextensionsv1.JSON `json:"metadata,omitempty"`
// +optional
// Used to define a conversion Strategy for the secret keys
// +kubebuilder:default="None"
ConversionStrategy PushSecretConversionStrategy `json:"conversionStrategy,omitempty"`
}
func (d PushSecretData) GetMetadata() *apiextensionsv1.JSON {

View file

@ -50,6 +50,14 @@ spec:
description: Secret Data that should be pushed to providers
items:
properties:
conversionStrategy:
default: None
description: Used to define a conversion Strategy for the secret
keys
enum:
- None
- ReverseUnicode
type: string
match:
description: Match a given Secret Key to be pushed to the provider.
properties:
@ -315,6 +323,14 @@ spec:
additionalProperties:
additionalProperties:
properties:
conversionStrategy:
default: None
description: Used to define a conversion Strategy for the
secret keys
enum:
- None
- ReverseUnicode
type: string
match:
description: Match a given Secret Key to be pushed to the
provider.

View file

@ -5547,6 +5547,13 @@ spec:
description: Secret Data that should be pushed to providers
items:
properties:
conversionStrategy:
default: None
description: Used to define a conversion Strategy for the secret keys
enum:
- None
- ReverseUnicode
type: string
match:
description: Match a given Secret Key to be pushed to the provider.
properties:
@ -5802,6 +5809,13 @@ spec:
additionalProperties:
additionalProperties:
properties:
conversionStrategy:
default: None
description: Used to define a conversion Strategy for the secret keys
enum:
- None
- ReverseUnicode
type: string
match:
description: Match a given Secret Key to be pushed to the provider.
properties:

View file

@ -40,3 +40,6 @@ This will _marshal_ the entire secret data and push it into this single property
!!! warning inline
This should _ONLY_ be done if the secret data is marshal-able. Values like, binary data cannot be marshaled and will result in error or invalid secret data.
### Key conversion strategy
You can also set `data[*].conversionStrategy: ReverseUnicode` to reverse the invalid character replaced by the `conversionStrategy: Unicode` configuration in the `ExternalSecret` object as [documented here](../guides/getallsecrets/#avoiding-name-conflicts).

View file

@ -29,7 +29,8 @@ spec:
items:
- key: config.yml
data:
- match:
- conversionStrategy: None # Also supports the ReverseUnicode strategy
match:
secretKey: best-pokemon # Source Kubernetes secret key to be pushed
remoteRef:
remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)

View file

@ -38,6 +38,7 @@ import (
"github.com/external-secrets/external-secrets/pkg/controllers/pushsecret/psmetrics"
"github.com/external-secrets/external-secrets/pkg/controllers/secretstore"
"github.com/external-secrets/external-secrets/pkg/provider/util/locks"
"github.com/external-secrets/external-secrets/pkg/utils"
)
const (
@ -47,6 +48,7 @@ const (
errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
errSetSecretFailed = "could not write remote ref %v to target secretstore %v: %v"
errFailedSetSecret = "set secret failed: %v"
errConvert = "could not apply conversion strategy to keys: %v"
pushSecretFinalizer = "pushsecret.externalsecrets.io/finalizer"
)
@ -289,11 +291,17 @@ func (r *Reconciler) handlePushSecretDataForStore(ctx context.Context, ps esapi.
Name: storeName,
Kind: refKind,
}
originalSecretData := secret.Data
secretClient, err := mgr.Get(ctx, storeRef, ps.GetNamespace(), nil)
if err != nil {
return out, fmt.Errorf("could not get secrets client for store %v: %w", storeName, err)
}
for _, data := range ps.Spec.Data {
secretData, err := utils.ReverseKeys(data.ConversionStrategy, originalSecretData)
if err != nil {
return nil, fmt.Errorf(errConvert, err)
}
secret.Data = secretData
key := data.GetSecretKey()
if !secretKeyExists(key, secret) {
return out, fmt.Errorf("secret key %v does not exist", key)

View file

@ -637,6 +637,64 @@ var _ = Describe("ExternalSecret controller", func() {
return true
}
}
// if conversion strategy is defined, revert the keys based on the strategy.
syncSuccessfullyWithConversionStrategy := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
return nil
}
tc.pushsecret = &v1alpha1.PushSecret{
ObjectMeta: metav1.ObjectMeta{
Name: PushSecretName,
Namespace: PushSecretNamespace,
},
Spec: v1alpha1.PushSecretSpec{
SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
{
Name: PushSecretStore,
Kind: "SecretStore",
},
},
Selector: v1alpha1.PushSecretSelector{
Secret: v1alpha1.PushSecretSecret{
Name: SecretName,
},
},
Data: []v1alpha1.PushSecretData{
{
ConversionStrategy: v1alpha1.PushSecretConversionReverseUnicode,
Match: v1alpha1.PushSecretMatch{
SecretKey: "some-array[0].entity",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "path/to/key",
},
},
},
},
},
}
tc.secret = &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: SecretName,
Namespace: PushSecretNamespace,
},
Data: map[string][]byte{
"some-array_U005b_0_U005d_.entity": []byte("value"),
},
}
tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
Eventually(func() bool {
By("checking if Provider value got updated")
secretValue := secret.Data["some-array_U005b_0_U005d_.entity"]
providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
if !ok {
return false
}
got := providerValue.Value
return bytes.Equal(got, secretValue)
}, time.Second*10, time.Second).Should(BeTrue())
return true
}
}
// if target Secret name is not specified it should use the ExternalSecret name.
syncMatchingLabels := func(tc *testCase) {
fakeProvider.SetSecretFn = func() error {
@ -937,6 +995,7 @@ var _ = Describe("ExternalSecret controller", func() {
Entry("should update the PushSecret status correctly if UpdatePolicy=IfNotExists", updateIfNotExistsSyncStatus),
Entry("should fail if secret existence cannot be verified if UpdatePolicy=IfNotExists", updateIfNotExistsSyncFailed),
Entry("should sync with template", syncSuccessfullyWithTemplate),
Entry("should sync with conversion strategy", syncSuccessfullyWithConversionStrategy),
Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
Entry("should track deletion tasks if Delete fails", failDelete),
Entry("should track deleted stores if Delete fails", failDeleteStore),

View file

@ -33,6 +33,7 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/template/v2"
@ -45,6 +46,7 @@ const (
var (
errKeyNotFound = errors.New("key not found")
unicodeRegex = regexp.MustCompile(`_U([0-9a-fA-F]{4,5})_`)
)
// JSONMarshal takes an interface and returns a new escaped and encoded byte slice.
@ -237,6 +239,48 @@ func convert(strategy esv1beta1.ExternalSecretConversionStrategy, str string) st
return strings.Join(newName, "")
}
// ReverseKeys reverses a secret map into a valid key map as expected by push secrets.
// Replaces the unicode encoded representation characters back to the actual unicode character depending on convert strategy.
func ReverseKeys(strategy esv1alpha1.PushSecretConversionStrategy, in map[string][]byte) (map[string][]byte, error) {
out := make(map[string][]byte, len(in))
for k, v := range in {
key := reverse(strategy, k)
if _, exists := out[key]; exists {
return nil, fmt.Errorf("secret name collision during conversion: %s", key)
}
out[key] = v
}
return out, nil
}
func reverse(strategy esv1alpha1.PushSecretConversionStrategy, str string) string {
switch strategy {
case esv1alpha1.PushSecretConversionReverseUnicode:
matches := unicodeRegex.FindAllStringSubmatchIndex(str, -1)
for i := len(matches) - 1; i >= 0; i-- {
match := matches[i]
start := match[0]
end := match[1]
unicodeHex := str[match[2]:match[3]]
unicodeInt, err := strconv.ParseInt(unicodeHex, 16, 32)
if err != nil {
continue // Skip invalid unicode representations
}
unicodeChar := fmt.Sprintf("%c", unicodeInt)
str = str[:start] + unicodeChar + str[end:]
}
return str
case esv1alpha1.PushSecretConversionNone:
return str
default:
return str
}
}
// MergeStringMap performs a deep clone from src to dest.
func MergeStringMap(dest, src map[string]string) {
for k, v := range src {

View file

@ -24,13 +24,17 @@ import (
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
const (
base64DecodedValue string = "foo%_?bar"
base64EncodedValue string = "Zm9vJV8/YmFy"
base64URLEncodedValue string = "Zm9vJV8_YmFy"
base64DecodedValue string = "foo%_?bar"
base64EncodedValue string = "Zm9vJV8/YmFy"
base64URLEncodedValue string = "Zm9vJV8_YmFy"
keyWithEmojis string = "😀foo😁bar😂baz😈bing"
keyWithInvalidChars string = "some-array[0].entity"
keyWithEncodedInvalidChars string = "some-array_U005b_0_U005d_.entity"
)
func TestObjectHash(t *testing.T) {
@ -212,7 +216,7 @@ func TestConvertKeys(t *testing.T) {
args: args{
strategy: esv1beta1.ExternalSecretConversionUnicode,
in: map[string][]byte{
"😀foo😁bar😂baz😈bing": []byte(`noop`),
keyWithEmojis: []byte(`noop`),
},
},
want: map[string][]byte{
@ -234,6 +238,77 @@ func TestConvertKeys(t *testing.T) {
}
}
func TestReverseKeys(t *testing.T) {
type args struct {
encodingStrategy esv1beta1.ExternalSecretConversionStrategy
decodingStrategy esv1alpha1.PushSecretConversionStrategy
in map[string][]byte
}
tests := []struct {
name string
args args
want map[string][]byte
wantErr bool
}{
{
name: "encoding and decoding strategy are selecting Unicode conversion and reverse unicode, so the in and want should match, this test covers Unicode characters beyond the Basic Multilingual Plane (BMP)",
args: args{
encodingStrategy: esv1beta1.ExternalSecretConversionUnicode,
decodingStrategy: esv1alpha1.PushSecretConversionReverseUnicode,
in: map[string][]byte{
keyWithEmojis: []byte(`noop`),
},
},
want: map[string][]byte{
keyWithEmojis: []byte(`noop`),
},
},
{
name: "encoding and decoding strategy are selecting Unicode conversion and reverse unicode, so the in and want should match, this test covers Unicode characters in the Basic Multilingual Plane (BMP)",
args: args{
encodingStrategy: esv1beta1.ExternalSecretConversionUnicode,
decodingStrategy: esv1alpha1.PushSecretConversionReverseUnicode,
in: map[string][]byte{
keyWithInvalidChars: []byte(`noop`),
},
},
want: map[string][]byte{
keyWithInvalidChars: []byte(`noop`),
},
},
{
name: "the encoding strategy is selecting Unicode conversion, but the decoding strategy is none, so we want an encoded representation of the content",
args: args{
encodingStrategy: esv1beta1.ExternalSecretConversionUnicode,
decodingStrategy: esv1alpha1.PushSecretConversionNone,
in: map[string][]byte{
keyWithInvalidChars: []byte(`noop`),
},
},
want: map[string][]byte{
keyWithEncodedInvalidChars: []byte(`noop`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ConvertKeys(tt.args.encodingStrategy, tt.args.in)
if (err != nil) != tt.wantErr {
t.Errorf("ConvertKeys() error = %v, wantErr %v", err, tt.wantErr)
return
}
got, err = ReverseKeys(tt.args.decodingStrategy, got)
if (err != nil) != tt.wantErr {
t.Errorf("ReverseKeys() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReverseKeys() = %v, want %v", got, tt.want)
}
})
}
}
func TestDecode(t *testing.T) {
type args struct {
strategy esv1beta1.ExternalSecretDecodingStrategy
@ -542,6 +617,50 @@ func TestRewrite(t *testing.T) {
}
}
func TestReverse(t *testing.T) {
type args struct {
strategy esv1alpha1.PushSecretConversionStrategy
in string
}
tests := []struct {
name string
args args
want string
}{
{
name: "do not change the key when using the None strategy",
args: args{
strategy: esv1alpha1.PushSecretConversionNone,
in: keyWithEncodedInvalidChars,
},
want: keyWithEncodedInvalidChars,
},
{
name: "reverse an unicode encoded key",
args: args{
strategy: esv1alpha1.PushSecretConversionReverseUnicode,
in: keyWithEncodedInvalidChars,
},
want: keyWithInvalidChars,
},
{
name: "do not attempt to decode an invalid unicode representation",
args: args{
strategy: esv1alpha1.PushSecretConversionReverseUnicode,
in: "_U0xxx_x_U005b_",
},
want: "_U0xxx_x[",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := reverse(tt.args.strategy, tt.args.in); got != tt.want {
t.Errorf("reverse() = %v, want %v", got, tt.want)
}
})
}
}
func TestFetchValueFromMetadata(t *testing.T) {
type args struct {
key string