Parcourir la source

feat(keeper): implement get secret by id or name (#6163)

Co-authored-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Co-authored-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Tiberiu il y a 1 semaine
Parent
commit
8b653d6082

+ 3 - 2
apis/externalsecrets/v1/secretstore_keepersecurity_types.go

@@ -20,6 +20,7 @@ import smmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 
 // KeeperSecurityProvider Configures a store to sync secrets using Keeper Security.
 type KeeperSecurityProvider struct {
-	Auth     smmeta.SecretKeySelector `json:"authRef"`
-	FolderID string                   `json:"folderID"`
+	Auth               smmeta.SecretKeySelector `json:"authRef"`
+	FolderID           string                   `json:"folderID"`
+	GetByTitleFallback bool                     `json:"getByTitleFallback,omitempty"`
 }

+ 2 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -3347,6 +3347,8 @@ spec:
                         type: object
                       folderID:
                         type: string
+                      getByTitleFallback:
+                        type: boolean
                     required:
                     - authRef
                     - folderID

+ 2 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -3347,6 +3347,8 @@ spec:
                         type: object
                       folderID:
                         type: string
+                      getByTitleFallback:
+                        type: boolean
                     required:
                     - authRef
                     - folderID

+ 4 - 0
deploy/crds/bundle.yaml

@@ -5367,6 +5367,8 @@ spec:
                           type: object
                         folderID:
                           type: string
+                        getByTitleFallback:
+                          type: boolean
                       required:
                         - authRef
                         - folderID
@@ -17471,6 +17473,8 @@ spec:
                           type: object
                         folderID:
                           type: string
+                        getByTitleFallback:
+                          type: boolean
                       required:
                         - authRef
                         - folderID

+ 10 - 0
docs/api/spec.md

@@ -6778,6 +6778,16 @@ string
 <td>
 </td>
 </tr>
+<tr>
+<td>
+<code>getByTitleFallback</code></br>
+<em>
+bool
+</em>
+</td>
+<td>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.KubernetesAuth">KubernetesAuth

+ 38 - 17
providers/v1/keepersecurity/client.go

@@ -67,8 +67,9 @@ const (
 
 // Client represents a KeeperSecurity client that can interact with the KeeperSecurity API.
 type Client struct {
-	ksmClient SecurityClient
-	folderID  string
+	ksmClient          SecurityClient
+	folderID           string
+	getByTitleFallback bool
 }
 
 // SecurityClient defines the interface for interacting with KeeperSecurity's API.
@@ -115,36 +116,56 @@ func (c *Client) Validate() (esv1.ValidationResult, error) {
 	return esv1.ValidationResultReady, nil
 }
 
-// GetSecret retrieves a secret from Keeper Security by its ID.
+// GetSecret retrieves a secret from Keeper Security by ID or name.
+// It first attempts to find the secret by ID, then falls back to name lookup.
+// The name lookup must be opted in by setting getByTitleFallback on the provider.
 func (c *Client) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	record, err := c.findSecretByID(ref.Key)
+	secret, err := c.findByIDWithNameFallback(ref.Key)
 	if err != nil {
 		return nil, err
 	}
-	secret, err := c.getValidKeeperSecret(record)
-	if err != nil {
-		return nil, err
-	}
-	// GetSecret retrieves a secret from Keeper Security by its ID.
-	// If ref.Property is specified, it returns only that property's value.
-
 	return secret.getItem(ref)
 }
 
 // GetSecretMap retrieves a secret from Keeper Security and returns it as a map.
 func (c *Client) GetSecretMap(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
-	record, err := c.findSecretByID(ref.Key)
+	secret, err := c.findByIDWithNameFallback(ref.Key)
 	if err != nil {
 		return nil, err
 	}
-	secret, err := c.getValidKeeperSecret(record)
+	return secret.getItems(ref)
+}
+
+// It first attempts to find the secret by ID, then falls back to name lookup.
+// The name lookup must be opted in by setting getByTitleFallback on the provider.
+func (c *Client) findByIDWithNameFallback(key string) (*Secret, error) {
+	record, err := c.findSecretByID(key)
 	if err != nil {
 		return nil, err
 	}
-	// GetSecretMap retrieves a secret from Keeper Security and returns it as a map.
-	// If ref.Property is specified, it returns only that property as a map entry.
 
-	return secret.getItems(ref)
+	if record == nil && c.getByTitleFallback {
+		records, err := c.ksmClient.GetSecretsByTitle(key)
+		if err != nil {
+			return nil, err
+		}
+
+		if len(records) > 1 {
+			return nil, errors.New(errKeeperSecuritySecretNotUnique)
+		} else if len(records) == 1 {
+			record = records[0]
+		}
+	}
+
+	if record == nil {
+		return nil, errors.New(errKeeperSecurityNoSecretsFound)
+	}
+
+	secret, err := c.getValidKeeperSecret(record)
+	if err != nil {
+		return nil, err
+	}
+	return secret, nil
 }
 
 // GetAllSecrets retrieves all secrets from Keeper Security that match the given criteria.
@@ -347,7 +368,7 @@ func (c *Client) findSecretByID(id string) (*ksm.Record, error) {
 	}
 
 	if len(records) == 0 {
-		return nil, errors.New(errKeeperSecurityNoSecretsFound)
+		return nil, nil
 	}
 
 	return records[0], nil

+ 152 - 14
providers/v1/keepersecurity/client_test.go

@@ -284,8 +284,9 @@ func TestClientGetAllSecrets(t *testing.T) {
 
 func TestClientGetSecret(t *testing.T) {
 	type fields struct {
-		ksmClient SecurityClient
-		folderID  string
+		ksmClient          SecurityClient
+		folderID           string
+		getByTitleFallback bool
 	}
 	type args struct {
 		ctx context.Context
@@ -306,7 +307,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecords()[0]}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -326,7 +328,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecords()[0]}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -345,7 +348,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecordWithLabels()}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -365,7 +369,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecordWithLabels()}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -384,7 +389,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecordWithLabels()}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -403,7 +409,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -415,14 +422,96 @@ func TestClientGetSecret(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name: "Get non existing secret",
+			name: "Get secret by ID",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
+					},
+				},
+				folderID:           folderID,
+				getByTitleFallback: false,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: esv1.ExternalSecretDataRemoteRef{
+					Key: record0,
+				},
+			},
+			want:    []byte(outputRecord0),
+			wantErr: false,
+		},
+		{
+			name: "Get secret by ID (with fallback)",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
+					},
+				},
+				folderID:           folderID,
+				getByTitleFallback: true,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: esv1.ExternalSecretDataRemoteRef{
+					Key: record0,
+				},
+			},
+			want:    []byte(outputRecord0),
+			wantErr: false,
+		},
+		{
+			name: "Get non existing secret with client error",
 			fields: fields{
 				ksmClient: &fake.MockKeeperClient{
 					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
 						return nil, errors.New("not found")
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: esv1.ExternalSecretDataRemoteRef{
+					Key: "record5",
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Get non existing secret",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{}, nil
+					},
+				},
+				folderID:           folderID,
+				getByTitleFallback: false,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: esv1.ExternalSecretDataRemoteRef{
+					Key: "record5",
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "Get non existing secret with fallback",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						return []*ksm.Record{}, nil
+					},
+					GetSecretsByTitleFn: func(recordTitle string) ([]*ksm.Record, error) {
+						return []*ksm.Record{}, nil
+					},
+				},
+				folderID:           folderID,
+				getByTitleFallback: true,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -440,7 +529,8 @@ func TestClientGetSecret(t *testing.T) {
 						return []*ksm.Record{generateRecords()[0]}, nil
 					},
 				},
-				folderID: folderID,
+				folderID:           folderID,
+				getByTitleFallback: false,
 			},
 			args: args{
 				ctx: context.Background(),
@@ -451,12 +541,60 @@ func TestClientGetSecret(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			name: "Get secret by name",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						// Return empty list to trigger name lookup
+						return []*ksm.Record{}, nil
+					},
+					GetSecretsByTitleFn: func(recordTitle string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0]}, nil
+					},
+				},
+				folderID:           folderID,
+				getByTitleFallback: true,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: esv1.ExternalSecretDataRemoteRef{
+					Key: record0,
+				},
+			},
+			want:    []byte(outputRecord0),
+			wantErr: false,
+		},
+		{
+			name: "Get secret by name with multiple matches",
+			fields: fields{
+				ksmClient: &fake.MockKeeperClient{
+					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
+						// Return empty list to trigger name lookup
+						return []*ksm.Record{}, nil
+					},
+					GetSecretsByTitleFn: func(recordTitle string) ([]*ksm.Record, error) {
+						return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
+					},
+				},
+				folderID:           folderID,
+				getByTitleFallback: true,
+			},
+			args: args{
+				ctx: context.Background(),
+				ref: esv1.ExternalSecretDataRemoteRef{
+					Key: record0,
+				},
+			},
+			wantErr: true,
+		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			c := &Client{
-				ksmClient: tt.fields.ksmClient,
-				folderID:  tt.fields.folderID,
+				ksmClient:          tt.fields.ksmClient,
+				folderID:           tt.fields.folderID,
+				getByTitleFallback: tt.fields.getByTitleFallback,
 			}
 			got, err := c.GetSecret(tt.args.ctx, tt.args.ref)
 			if (err != nil) != tt.wantErr {
@@ -581,7 +719,7 @@ func TestClientGetSecretMap(t *testing.T) {
 			fields: fields{
 				ksmClient: &fake.MockKeeperClient{
 					GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
-						return nil, errors.New("not found")
+						return nil, errors.New(errKeeperSecurityNoSecretsFound)
 					},
 				},
 				folderID: folderID,

+ 3 - 2
providers/v1/keepersecurity/provider.go

@@ -72,8 +72,9 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 	}
 	ksmClient := ksm.NewSecretsManager(ksmClientOptions)
 	client := &Client{
-		folderID:  keeperStore.FolderID,
-		ksmClient: ksmClient,
+		folderID:           keeperStore.FolderID,
+		ksmClient:          ksmClient,
+		getByTitleFallback: keeperStore.GetByTitleFallback,
 	}
 
 	return client, nil

+ 1 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -501,6 +501,7 @@ spec:
         name: string
         namespace: string
       folderID: string
+      getByTitleFallback: true
     kubernetes:
       auth:
         cert:

+ 1 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -501,6 +501,7 @@ spec:
         name: string
         namespace: string
       folderID: string
+      getByTitleFallback: true
     kubernetes:
       auth:
         cert: