From 4a14e89c7a41085c97f98c9cac125b9763b9864d Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 19 Sep 2025 19:00:18 -0400 Subject: [PATCH 01/28] Add Akeyless Secrets Store Component Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 59 ++++++++ secretstores/akeyless/akeyless.go | 175 ++++++++++++++++++++++ secretstores/akeyless/akeyless_test.go | 199 +++++++++++++++++++++++++ secretstores/akeyless/example.yaml | 12 ++ secretstores/akeyless/metadata.yaml | 23 +++ 5 files changed, 468 insertions(+) create mode 100644 secretstores/akeyless/README.md create mode 100644 secretstores/akeyless/akeyless.go create mode 100644 secretstores/akeyless/akeyless_test.go create mode 100644 secretstores/akeyless/example.yaml create mode 100644 secretstores/akeyless/metadata.yaml diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md new file mode 100644 index 0000000000..cdd915354f --- /dev/null +++ b/secretstores/akeyless/README.md @@ -0,0 +1,59 @@ +# Akeyless Secret Store + +This component provides a Dapr secret store implementation for [Akeyless](https://www.akeyless.io/), a cloud-native secrets management platform. + +## Configuration + +The Akeyless secret store component supports the following configuration options: + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `gatewayUrl` | No | The Akeyless Gateway URL. If not provided, uses the default Akeyless cloud URL. | `https://your-gateway.akeyless.io` | +| `token` | Yes | The Akeyless authentication token. | `your-akeyless-token` | + +## Example Configuration + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" + - name: token + value: "your-akeyless-token" +``` + +## Usage + +Once configured, you can retrieve secrets using the Dapr secrets API: + +```bash +# Get a single secret +curl http://localhost:3500/v1.0/secrets/akeyless-secretstore/my-secret + +# Get all secrets +curl http://localhost:3500/v1.0/secrets/akeyless-secretstore +``` + +## Features + +- **GetSecret**: Retrieve individual secrets by name +- **BulkGetSecret**: Retrieve all secrets from the Akeyless vault +- **Authentication**: Supports Akeyless token-based authentication +- **Custom Gateway**: Supports custom Akeyless Gateway URLs + +## Requirements + +- Akeyless account and authentication token +- Akeyless Go SDK v5 (automatically included as a dependency) + +## Authentication + +The component uses Akeyless token-based authentication. You can obtain a token from your Akeyless dashboard or by using the Akeyless CLI. + +For more information about Akeyless authentication, see the [Akeyless documentation](https://docs.akeyless.io/). diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go new file mode 100644 index 0000000000..4bb6e24e6d --- /dev/null +++ b/secretstores/akeyless/akeyless.go @@ -0,0 +1,175 @@ +package akeyless + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/akeylesslabs/akeyless-go/v5" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" + kitmd "github.com/dapr/kit/metadata" +) + +var _ secretstores.SecretStore = (*akeylessSecretStore)(nil) + +// akeylessSecretStore is a secret store implementation for Akeyless. +type akeylessSecretStore struct { + client *akeyless.APIClient + token string + logger logger.Logger +} + +// NewAkeylessSecretStore returns a new Akeyless secret store. +func NewAkeylessSecretStore(logger logger.Logger) secretstores.SecretStore { + return &akeylessSecretStore{ + logger: logger, + } +} + +// akeylessMetadata contains the metadata for the Akeyless secret store. +type akeylessMetadata struct { + GatewayURL string `json:"gatewayUrl" mapstructure:"gatewayUrl"` + Token string `json:"token" mapstructure:"token"` +} + +// Init creates a new Akeyless secret store client. +func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metadata) error { + m, err := a.parseMetadata(meta) + if err != nil { + return err + } + + // Set up Akeyless configuration + config := akeyless.NewConfiguration() + if m.GatewayURL != "" { + config.Servers = []akeyless.ServerConfiguration{ + { + URL: m.GatewayURL, + }, + } + } + + // Create the API client + a.client = akeyless.NewAPIClient(config) + a.token = m.Token + + return nil +} + +// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. +func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { + if a.client == nil { + return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") + } + + // Create the get secret value request + getSecretValue := akeyless.NewGetSecretValue([]string{req.Name}) + getSecretValue.SetToken(a.token) + + // Execute the request + result, _, err := a.client.V2Api.GetSecretValue(ctx).Body(*getSecretValue).Execute() + if err != nil { + return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get secret from Akeyless: %w", err) + } + + // Extract the secret value + secretValue, exists := result[req.Name] + if !exists { + return secretstores.GetSecretResponse{}, fmt.Errorf("secret '%s' not found", req.Name) + } + + // Convert the secret value to string + var secretStr string + if str, ok := secretValue.(string); ok { + secretStr = str + } else { + // If it's not a string, convert it to JSON string + secretBytes, err := json.Marshal(secretValue) + if err != nil { + return secretstores.GetSecretResponse{}, fmt.Errorf("failed to convert secret value to string: %w", err) + } + secretStr = string(secretBytes) + } + + // Return the secret in the expected format + return secretstores.GetSecretResponse{ + Data: map[string]string{ + req.Name: secretStr, + }, + }, nil +} + +// BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. +func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) { + if a.client == nil { + return secretstores.BulkGetSecretResponse{}, errors.New("akeyless client not initialized") + } + + // For bulk get, we need to list all secrets first + listItems := akeyless.NewListItems() + listItems.SetToken(a.token) + + // Execute the list items request + itemsList, _, err := a.client.V2Api.ListItems(ctx).Body(*listItems).Execute() + if err != nil { + return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to list items from Akeyless: %w", err) + } + + // Get all secret values + allSecrets := make(map[string]map[string]string) + for _, item := range itemsList.Items { + if item.ItemName == nil { + continue + } + secretName := *item.ItemName + secretResp, err := a.GetSecret(ctx, secretstores.GetSecretRequest{ + Name: secretName, + Metadata: req.Metadata, + }) + if err != nil { + a.logger.Warnf("Failed to get secret '%s': %v", secretName, err) + continue + } + allSecrets[secretName] = secretResp.Data + } + + return secretstores.BulkGetSecretResponse{ + Data: allSecrets, + }, nil +} + +// Features returns the features available in this secret store. +func (a *akeylessSecretStore) Features() []secretstores.Feature { + return []secretstores.Feature{} +} + +// GetComponentMetadata returns the component metadata. +func (a *akeylessSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := akeylessMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) + return +} + +// Close closes the secret store. +func (a *akeylessSecretStore) Close() error { + return nil +} + +// parseMetadata parses the metadata from the component configuration. +func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeylessMetadata, error) { + var m akeylessMetadata + err := kitmd.DecodeMetadata(meta.Properties, &m) + if err != nil { + return nil, err + } + + if m.Token == "" { + return nil, errors.New("token is required") + } + + return &m, nil +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go new file mode 100644 index 0000000000..07965a7d31 --- /dev/null +++ b/secretstores/akeyless/akeyless_test.go @@ -0,0 +1,199 @@ +package akeyless + +import ( + "context" + "testing" + + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testToken = "test-token" + testGatewayURL = "https://test-gateway.akeyless.io" +) + +func TestNewAkeylessSecretStore(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + assert.NotNil(t, store) +} + +func TestInit(t *testing.T) { + tests := []struct { + name string + metadata secretstores.Metadata + expectError bool + }{ + { + name: "valid metadata with token", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "token": testToken, + }, + }, + }, + expectError: false, + }, + { + name: "valid metadata with token and gateway URL", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "token": testToken, + "gatewayUrl": testGatewayURL, + }, + }, + }, + expectError: false, + }, + { + name: "missing token", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{}, + }, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + err := store.Init(context.Background(), tt.metadata) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, store.client) + assert.Equal(t, "test-token", store.token) + } + }) + } +} + +func TestGetSecretWithoutInit(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + req := secretstores.GetSecretRequest{ + Name: "test-secret", + } + + _, err := store.GetSecret(context.Background(), req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestBulkGetSecretWithoutInit(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + req := secretstores.BulkGetSecretRequest{} + + _, err := store.BulkGetSecret(context.Background(), req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestFeatures(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + + features := store.Features() + assert.Empty(t, features) +} + +func TestClose(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log) + + err := store.Close() + assert.NoError(t, err) +} + +func TestParseMetadata(t *testing.T) { + tests := []struct { + name string + properties map[string]string + expectError bool + expected *akeylessMetadata + }{ + { + name: "valid metadata with token only", + properties: map[string]string{ + "token": testToken, + }, + expectError: false, + expected: &akeylessMetadata{ + Token: testToken, + }, + }, + { + name: "valid metadata with token and gateway URL", + properties: map[string]string{ + "token": testToken, + "gatewayUrl": testGatewayURL, + }, + expectError: false, + expected: &akeylessMetadata{ + Token: testToken, + GatewayURL: testGatewayURL, + }, + }, + { + name: "missing token", + properties: map[string]string{ + "gatewayUrl": testGatewayURL, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: tt.properties, + }, + } + + result, err := store.parseMetadata(meta) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +func TestGetComponentMetadata(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + metadata := store.GetComponentMetadata() + require.NotNil(t, metadata) + + // Check that the metadata contains the expected fields + assert.Contains(t, metadata, "gatewayUrl") + assert.Contains(t, metadata, "token") + + // Check that the metadata fields exist + tokenField := metadata["token"] + require.NotNil(t, tokenField) + + gatewayField := metadata["gatewayUrl"] + require.NotNil(t, gatewayField) +} diff --git a/secretstores/akeyless/example.yaml b/secretstores/akeyless/example.yaml new file mode 100644 index 0000000000..8ca7bb0fe0 --- /dev/null +++ b/secretstores/akeyless/example.yaml @@ -0,0 +1,12 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" # Optional: defaults to Akeyless cloud + - name: token + value: "your-akeyless-token" # Required: your Akeyless authentication token diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml new file mode 100644 index 0000000000..f6be3b8e21 --- /dev/null +++ b/secretstores/akeyless/metadata.yaml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=../../../component-metadata-schema.json +schemaVersion: v1 +type: secretstores +name: akeyless +version: v1 +status: beta +title: "Akeyless Secret Store" +urls: + - title: Reference + url: https://docs.dapr.io/reference/components-reference/supported-secret-stores/akeyless/ +metadata: + - name: gatewayUrl + required: false + description: | + The Akeyless Gateway URL. If not provided, uses the default Akeyless cloud URL. + example: "https://your.akeyless.gw" + type: string + - name: token + required: true + description: | + The Akeyless authentication token (t-token). + example: "t-ABCD1234" + type: string From 8ab2a41e4dbdf4209d558ec17e07a11a19904312 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 24 Sep 2025 16:21:26 -0400 Subject: [PATCH 02/28] mv metadata validation to parsing func add aws iam, jwt support Signed-off-by: Kobbi Gal --- go.mod | 2 + go.sum | 83 ++++++++ secretstores/akeyless/README.md | 66 ++++-- secretstores/akeyless/akeyless.go | 122 ++++++++--- secretstores/akeyless/akeyless_test.go | 273 ++++++++++++++++++++++--- secretstores/akeyless/example.yaml | 10 +- secretstores/akeyless/metadata.yaml | 25 ++- secretstores/akeyless/utils.go | 133 ++++++++++++ 8 files changed, 627 insertions(+), 87 deletions(-) create mode 100644 secretstores/akeyless/utils.go diff --git a/go.mod b/go.mod index 8842dd879f..7f17d46859 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/IBM/sarama v1.45.2 github.com/aerospike/aerospike-client-go/v6 v6.12.0 + github.com/akeylesslabs/akeyless-go/v5 v5.0.8 github.com/alibaba/sentinel-golang v1.0.4 github.com/alibabacloud-go/darabonba-openapi v0.2.1 github.com/alibabacloud-go/oos-20190601 v1.0.4 @@ -177,6 +178,7 @@ require ( github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/ajg/form v1.5.1 // indirect + github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect diff --git a/go.sum b/go.sum index 4aace7746b..583aba3064 100644 --- a/go.sum +++ b/go.sum @@ -15,6 +15,11 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= @@ -81,8 +86,11 @@ github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.6.0 h1:FQOmDxJj1If0D0khZR00MDa2Eb+k9BBsSaK7cEbLwkk= github.com/Azure/azure-sdk-for-go/sdk/ai/azopenai v0.6.0/go.mod h1:X0+PSrHOZdTjkiEhgv53HS5gplbzVVl2jd6hQRYSS3c= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= github.com/Azure/azure-sdk-for-go/sdk/data/azappconfig v1.1.0 h1:AdaGDU3FgoUC2tsd3vsd9JblRrpFLUsS38yh1eLYfwM= @@ -91,6 +99,8 @@ github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.0.3 h1:gBWC0dYF3aO+7xGxL0 github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos v1.0.3/go.mod h1:7LBWaO4KRASAo9VpfhpxQKkdY6PBwkv9UDKzL9Sajuw= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.2.0 h1:aJG+Jxd9/rrLwf8R1Ko0RlOBTJASs/lGQJ8b9AdlKTc= github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.2.0/go.mod h1:41ONblJrPxDcnVr+voS+3xXWy/KnZLh+7zY5s6woAlQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0 h1:jBQA3cKT4L2rWMpgE7Yt3Hwh2aUj8KXjIGLxjHeYNNo= github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg= github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.2.1 h1:0f6XnzroY1yCQQwxGf/n/2xlaBF02Qhof2as99dGNsY= @@ -121,6 +131,7 @@ github.com/Azure/go-amqp v1.0.5 h1:po5+ljlcNSU8xtapHTe8gIc8yHxCzC03E8afH2g1ftU= github.com/Azure/go-amqp v1.0.5/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -175,6 +186,10 @@ github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 h1:ly0WKARATneFzwBlTZ2lUyjtLqoOEYqt1vOlf89za/4= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5/go.mod h1:W6DMNwPyIE3jpXDaJOvCKUT/kHPZrpl/BGiIVUILbMk= +github.com/akeylesslabs/akeyless-go/v5 v5.0.8 h1:rOYxuhCHEZDt7e5J1agI2I4qNPueX28dL4PdTv1rhu0= +github.com/akeylesslabs/akeyless-go/v5 v5.0.8/go.mod h1:4oo5+/uOcshVr/+hLxxL4UQIALyQNWwOCskLGgTL6nk= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -271,6 +286,7 @@ github.com/aws/aws-msk-iam-sasl-signer-go v1.0.1-0.20241125194140-078c08b8574a/g github.com/aws/aws-sdk-go v1.19.48/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.32.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= +github.com/aws/aws-sdk-go v1.41.13/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= @@ -466,6 +482,7 @@ github.com/cloudwego/thriftgo v0.2.8/go.mod h1:dAyXHEmKXo0LfMCrblVEY3mUZsdeuA5+i github.com/cloudwego/thriftgo v0.3.0 h1:BBb9hVcqmu9p4iKUP/PSIaDB21Vfutgd7k2zgK37Q9Q= github.com/cloudwego/thriftgo v0.3.0/go.mod h1:AvH0iEjvKHu3cdxG7JvhSAaffkS4h2f4/ZxpJbm48W4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= @@ -548,6 +565,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0= github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY= github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -595,6 +614,7 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= @@ -773,6 +793,7 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= @@ -814,6 +835,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -867,6 +889,7 @@ github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -876,6 +899,10 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20220608213341-c488b8fa1db3/go.mod h1:gSuNB+gJaOiQKLEZ+q+PK9Mq3SOzhRcw2GsGS/FhYDk= github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= @@ -887,6 +914,7 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= @@ -1288,6 +1316,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= @@ -1428,6 +1457,7 @@ github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9F github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -1790,6 +1820,7 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6-0.20201102222123-380f4078db9f/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -1912,7 +1943,9 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= @@ -1966,6 +1999,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= @@ -2017,8 +2052,11 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= @@ -2047,7 +2085,9 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2055,6 +2095,13 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= @@ -2132,28 +2179,35 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201223074533-0d417f636930/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2207,6 +2261,7 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= @@ -2217,6 +2272,7 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -2301,9 +2357,14 @@ golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201014170642-d1624618ad65/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -2348,6 +2409,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -2357,6 +2424,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -2392,7 +2460,18 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20211104193956-4c6863e31247/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= @@ -2423,11 +2502,15 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index cdd915354f..042c5447d0 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -8,10 +8,12 @@ The Akeyless secret store component supports the following configuration options | Field | Required | Description | Example | |-------|----------|-------------|---------| -| `gatewayUrl` | No | The Akeyless Gateway URL. If not provided, uses the default Akeyless cloud URL. | `https://your-gateway.akeyless.io` | -| `token` | Yes | The Akeyless authentication token. | `your-akeyless-token` | +| `gatewayUrl` | No | The Akeyless Gateway URL. Default is https://api.akeyless.io. | `https://your-gateway.akeyless.io` | +| `accessId` | Yes | The Akeyless authentication access ID. | `p-123456780wm` | +| `jwt` | No | If using an OAuth2.0/JWT access ID, specify the JSON Web Token | `eyJ...` | +| `accessKey` | No | If using an API Key access ID, specify the API key | `ABCD123...=` | -## Example Configuration +## Example Configuration: API Key ```yaml apiVersion: dapr.io/v1alpha1 @@ -24,8 +26,47 @@ spec: metadata: - name: gatewayUrl value: "https://your-gateway.akeyless.io" - - name: token - value: "your-akeyless-token" + - name: accessId + value: "p-1234Abcdam" + - name: accessKey + value: "ABCD1233...=" +``` + + +## Example Configuration: JWT + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" + - name: accessId + value: "p-1234Abcdom" + - name: jwt + value: "eyJ" +``` + +## Example Configuration: AWS IAM + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless-secretstore +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" + - name: accessId + value: "p-1234Abcdwm" ``` ## Usage @@ -37,23 +78,10 @@ Once configured, you can retrieve secrets using the Dapr secrets API: curl http://localhost:3500/v1.0/secrets/akeyless-secretstore/my-secret # Get all secrets -curl http://localhost:3500/v1.0/secrets/akeyless-secretstore +curl http://localhost:3500/v1.0/secrets/akeyless-secretstore/bulk ``` ## Features - **GetSecret**: Retrieve individual secrets by name - **BulkGetSecret**: Retrieve all secrets from the Akeyless vault -- **Authentication**: Supports Akeyless token-based authentication -- **Custom Gateway**: Supports custom Akeyless Gateway URLs - -## Requirements - -- Akeyless account and authentication token -- Akeyless Go SDK v5 (automatically included as a dependency) - -## Authentication - -The component uses Akeyless token-based authentication. You can obtain a token from your Akeyless dashboard or by using the Akeyless CLI. - -For more information about Akeyless authentication, see the [Akeyless documentation](https://docs.akeyless.io/). diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 4bb6e24e6d..b4cdb0e8a4 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -8,6 +8,7 @@ import ( "reflect" "github.com/akeylesslabs/akeyless-go/v5" + "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/secretstores" "github.com/dapr/kit/logger" @@ -18,7 +19,7 @@ var _ secretstores.SecretStore = (*akeylessSecretStore)(nil) // akeylessSecretStore is a secret store implementation for Akeyless. type akeylessSecretStore struct { - client *akeyless.APIClient + v2 *akeyless.V2ApiService token string logger logger.Logger } @@ -33,36 +34,33 @@ func NewAkeylessSecretStore(logger logger.Logger) secretstores.SecretStore { // akeylessMetadata contains the metadata for the Akeyless secret store. type akeylessMetadata struct { GatewayURL string `json:"gatewayUrl" mapstructure:"gatewayUrl"` - Token string `json:"token" mapstructure:"token"` + JWT string `json:"jwt" mapstructure:"jwt"` + AccessID string `json:"accessId" mapstructure:"accessId"` + AccessKey string `json:"accessKey" mapstructure:"accessKey"` + AccessType string `json:"accessType" mapstructure:"accessType"` } -// Init creates a new Akeyless secret store client. +// Init creates a new Akeyless secret store client and sets up the Akeyless API client +// with authentication method based on the accessId. func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metadata) error { + a.logger.Info("Initializing Akeyless secret store...") + a.logger.Info("Parsing metadata...") m, err := a.parseMetadata(meta) if err != nil { - return err + return errors.New("failed to parse metadata: " + err.Error()) } - // Set up Akeyless configuration - config := akeyless.NewConfiguration() - if m.GatewayURL != "" { - config.Servers = []akeyless.ServerConfiguration{ - { - URL: m.GatewayURL, - }, - } + err = Authenticate(m, a) + if err != nil { + return errors.New("failed to authenticate with Akeyless: " + err.Error()) } - // Create the API client - a.client = akeyless.NewAPIClient(config) - a.token = m.Token - return nil } // GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { - if a.client == nil { + if a.v2 == nil { return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") } @@ -71,7 +69,7 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge getSecretValue.SetToken(a.token) // Execute the request - result, _, err := a.client.V2Api.GetSecretValue(ctx).Body(*getSecretValue).Execute() + result, _, err := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() if err != nil { return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get secret from Akeyless: %w", err) } @@ -105,7 +103,7 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge // BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) { - if a.client == nil { + if a.v2 == nil { return secretstores.BulkGetSecretResponse{}, errors.New("akeyless client not initialized") } @@ -114,27 +112,48 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore listItems.SetToken(a.token) // Execute the list items request - itemsList, _, err := a.client.V2Api.ListItems(ctx).Body(*listItems).Execute() + itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() if err != nil { return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to list items from Akeyless: %w", err) } - // Get all secret values + // Create a map to store all secrets for response allSecrets := make(map[string]map[string]string) + + // Create a list of item names from all items + itemsNames := make([]string, 0, len(itemsList.Items)) for _, item := range itemsList.Items { if item.ItemName == nil { continue } - secretName := *item.ItemName - secretResp, err := a.GetSecret(ctx, secretstores.GetSecretRequest{ - Name: secretName, - Metadata: req.Metadata, - }) - if err != nil { - a.logger.Warnf("Failed to get secret '%s': %v", secretName, err) - continue + itemsNames = append(itemsNames, *item.ItemName) + } + + // Get all secrets + getSecretValue := akeyless.NewGetSecretValue(itemsNames) + getSecretValue.SetToken(a.token) + secretResp, _, err := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() + if err != nil { + return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to get secrets from Akeyless: %w", err) + } + + // Add all secrets to the response + for name, secret := range secretResp { + // secret is of type interface{}, need to assert to map[string]interface{} first + secretMap, ok := secret.(map[string]any) + if !ok { + return secretstores.BulkGetSecretResponse{}, fmt.Errorf("unexpected secret type for %s", name) + } + // Convert map[string]interface{} to map[string]string + secretStrMap := make(map[string]string) + for k, v := range secretMap { + if v == nil { + secretStrMap[k] = "" + } else { + secretStrMap[k] = fmt.Sprintf("%v", v) + } } - allSecrets[secretName] = secretResp.Data + allSecrets[name] = secretStrMap } return secretstores.BulkGetSecretResponse{ @@ -161,14 +180,53 @@ func (a *akeylessSecretStore) Close() error { // parseMetadata parses the metadata from the component configuration. func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeylessMetadata, error) { + + a.logger.Debug("Parsing metadata...") var m akeylessMetadata err := kitmd.DecodeMetadata(meta.Properties, &m) if err != nil { return nil, err } - if m.Token == "" { - return nil, errors.New("token is required") + // Validate access ID + if m.AccessID == "" { + return nil, errors.New("accessId is required") + } + + if !IsValidAccessIdFormat(m.AccessID) { + return nil, errors.New("invalid accessId format, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + + // Get the authentication method + a.logger.Debug("extracting access type from accessId...") + accessTypeChar, err := ExtractAccessTypeChar(m.AccessID) + if err != nil { + return nil, errors.New("unable to extract access type character from accessId, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + + a.logger.Debug("getting access type display name for character %s...", accessTypeChar) + accessTypeDisplayName, err := GetAccessTypeDisplayName(accessTypeChar) + if err != nil { + return nil, errors.New("unable to get access type display name, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + } + a.logger.Debug("access type detected: %s", accessTypeDisplayName) + + switch accessTypeDisplayName { + case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + if m.AccessKey == "" { + return nil, errors.New("accessKey is required") + } + case AKEYLESS_AUTH_ACCESS_JWT: + if m.JWT == "" { + return nil, errors.New("jwt is required") + } + } + m.AccessType = accessTypeDisplayName + + // Set default gateway URL if not specified + if m.GatewayURL == "" { + a.logger.Info("Gateway URL is not set, using default value %s...", AKEYLESS_PUBLIC_GATEWAY_URL) + m.GatewayURL = AKEYLESS_PUBLIC_GATEWAY_URL } return &m, nil diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 07965a7d31..c921d8dbb7 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -2,8 +2,14 @@ package akeyless import ( "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" "testing" + "github.com/akeylesslabs/akeyless-go/v5" "github.com/dapr/components-contrib/metadata" "github.com/dapr/components-contrib/secretstores" "github.com/dapr/kit/logger" @@ -12,10 +18,113 @@ import ( ) const ( - testToken = "test-token" - testGatewayURL = "https://test-gateway.akeyless.io" + testAccessIdIAM = "p-xt3sT2nah7gpwm" + testAccessIdJwt = "p-xt3sT2nah7gpom" + testAccessIdKey = "p-xt3sT2nah7gpam" + testAccessKey = "ABCD1233xxx=" + // { + // "sub": "1234567890", + // "name": "John Doe", + // "iat": 1516239022 + // } + testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" ) +// Global mock server for all tests +var mockGateway *httptest.Server + +// Mock AWS cloud ID for testing +const mockCloudID = "123456789012" + +// mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID +func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + akeylessSecretStore.logger.Debug("Creating authentication request to Akeyless...") + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + authRequest.SetAccessType(metadata.AccessType) + + // Depending on the access type we set the appropriate authentication method + switch metadata.AccessType { + // If access type is AWS IAM we use the mock cloud ID + case AKEYLESS_AUTH_ACCESS_IAM: + akeylessSecretStore.logger.Debug("Using mock cloud ID for AWS IAM...") + authRequest.SetCloudId(mockCloudID) + case AKEYLESS_AUTH_ACCESS_JWT: + akeylessSecretStore.logger.Debug("Setting JWT for authentication...") + authRequest.SetJwt(metadata.JWT) + case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + akeylessSecretStore.logger.Debug("Setting access key for authentication...") + authRequest.SetAccessKey(metadata.AccessKey) + } + + // Create Akeyless API client configuration + akeylessSecretStore.logger.Debug("Creating Akeyless API client configuration...") + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = AKEYLESS_USER_AGENT + config.AddDefaultHeader("akeylessclienttype", AKEYLESS_USER_AGENT) + + akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api + + akeylessSecretStore.logger.Debug("Authenticating with Akeyless...") + out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() + if err != nil { + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + + akeylessSecretStore.logger.Debug("Setting token %s for authentication...", out.GetToken()[:5]+"[REDACTED]") + akeylessSecretStore.logger.Debug("Expires at: %s", out.GetExpiration()) + akeylessSecretStore.token = out.GetToken() + + return nil +} + +// TestMain sets up and tears down the mock server for all tests +func TestMain(m *testing.M) { + // Setup mock server that returns an *akeyless.AuthOutput + mockGateway = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := map[string]interface{}{ + "token": "t-1234567890", + "expiration": "2025-01-01T00:00:00Z", + } + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/get-secret-value", "/v2/get-secret-value": + // Return a mock secret value response + secretResponse := map[string]interface{}{ + "my-secret": "secret-value-123", + } + jsonResponse, _ := json.Marshal(secretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + // Run tests + code := m.Run() + + // Cleanup + mockGateway.Close() + + // Exit with the same code as the tests + os.Exit(code) +} + func TestNewAkeylessSecretStore(t *testing.T) { log := logger.NewLogger("test") store := NewAkeylessSecretStore(log) @@ -29,33 +138,50 @@ func TestInit(t *testing.T) { expectError bool }{ { - name: "valid metadata with token", + name: "gw, access id and key", metadata: secretstores.Metadata{ Base: metadata.Base{ Properties: map[string]string{ - "token": testToken, + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, }, }, }, expectError: false, }, { - name: "valid metadata with token and gateway URL", + name: "gw, access id and jwt", metadata: secretstores.Metadata{ Base: metadata.Base{ Properties: map[string]string{ - "token": testToken, - "gatewayUrl": testGatewayURL, + "accessId": testAccessIdJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, }, }, }, expectError: false, }, { - name: "missing token", + name: "gw, access id (aws_iam)", metadata: secretstores.Metadata{ Base: metadata.Base{ - Properties: map[string]string{}, + Properties: map[string]string{ + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "missing access id", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "gatewayUrl": mockGateway.URL, + }, }, }, expectError: true, @@ -67,13 +193,33 @@ func TestInit(t *testing.T) { log := logger.NewLogger("test") store := NewAkeylessSecretStore(log).(*akeylessSecretStore) - err := store.Init(context.Background(), tt.metadata) - if tt.expectError { - assert.Error(t, err) + tt.metadata.Properties["gatewayUrl"] = mockGateway.URL + + // For AWS IAM test, use mock authentication to avoid AWS dependency + if tt.name == "gw, access id (aws_iam)" { + // Parse metadata first + m, err := store.parseMetadata(tt.metadata) + require.NoError(t, err) + + // Use mock authentication instead of the real one + err = mockAuthenticate(m, store) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + } } else { - assert.NoError(t, err) - assert.NotNil(t, store.client) - assert.Equal(t, "test-token", store.token) + // Use normal Init for other test cases + err := store.Init(context.Background(), tt.metadata) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + } } }) } @@ -127,31 +273,51 @@ func TestParseMetadata(t *testing.T) { expected *akeylessMetadata }{ { - name: "valid metadata with token only", + name: "valid metadata with access id and key", + properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdKey, + AccessKey: testAccessKey, + AccessType: AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE, + GatewayURL: "https://api.akeyless.io", // Default gateway URL + }, + }, + { + name: "valid metadata with access id and jwt", properties: map[string]string{ - "token": testToken, + "accessId": testAccessIdJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, }, expectError: false, expected: &akeylessMetadata{ - Token: testToken, + AccessID: testAccessIdJwt, + JWT: testJWT, + AccessType: AKEYLESS_AUTH_ACCESS_JWT, + GatewayURL: mockGateway.URL, }, }, { - name: "valid metadata with token and gateway URL", + name: "valid metadata with access id (aws_iam)", properties: map[string]string{ - "token": testToken, - "gatewayUrl": testGatewayURL, + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, }, expectError: false, expected: &akeylessMetadata{ - Token: testToken, - GatewayURL: testGatewayURL, + AccessID: testAccessIdIAM, + AccessType: AKEYLESS_AUTH_ACCESS_IAM, + GatewayURL: mockGateway.URL, }, }, { - name: "missing token", + name: "missing access id", properties: map[string]string{ - "gatewayUrl": testGatewayURL, + "gatewayUrl": mockGateway.URL, }, expectError: true, }, @@ -188,12 +354,63 @@ func TestGetComponentMetadata(t *testing.T) { // Check that the metadata contains the expected fields assert.Contains(t, metadata, "gatewayUrl") - assert.Contains(t, metadata, "token") + assert.Contains(t, metadata, "accessId") + assert.Contains(t, metadata, "jwt") + assert.Contains(t, metadata, "accessKey") // Check that the metadata fields exist - tokenField := metadata["token"] - require.NotNil(t, tokenField) + accessIdField := metadata["accessId"] + require.NotNil(t, accessIdField) gatewayField := metadata["gatewayUrl"] require.NotNil(t, gatewayField) } + +func TestMockServerReturnsAuthOutput(t *testing.T) { + // Test that the mock server properly returns an AuthOutput response + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + // Test with access key authentication + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + assert.Equal(t, "t-1234567890", store.token) +} + +func TestMockAWSCloudID(t *testing.T) { + // Test that the mock AWS cloud ID works correctly + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + // Test with AWS IAM authentication using mock cloud ID + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + // Parse metadata first + m, err := store.parseMetadata(meta) + require.NoError(t, err) + assert.Equal(t, AKEYLESS_AUTH_ACCESS_IAM, m.AccessType) + + // Use mock authentication with mock cloud ID + err = mockAuthenticate(m, store) + assert.NoError(t, err) + assert.NotNil(t, store.v2) + assert.NotNil(t, store.token) + assert.Equal(t, "t-1234567890", store.token) +} diff --git a/secretstores/akeyless/example.yaml b/secretstores/akeyless/example.yaml index 8ca7bb0fe0..a43157c38b 100644 --- a/secretstores/akeyless/example.yaml +++ b/secretstores/akeyless/example.yaml @@ -7,6 +7,10 @@ spec: version: v1 metadata: - name: gatewayUrl - value: "https://your-gateway.akeyless.io" # Optional: defaults to Akeyless cloud - - name: token - value: "your-akeyless-token" # Required: your Akeyless authentication token + value: "https://your-gateway.akeyless.io" # Optional: defaults to https://api.akeyless.io + - name: accessId + value: "p-123456780wm" # Required: your Akeyless access ID + - name: accessKey + value: "ABCD1233...=" # Optional: your Akeyless access key for API key authentication + - name: jwt + value: "eyJ..." # Required: your Akeyless JWT for JWT authentication diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml index f6be3b8e21..ae6f794f4c 100644 --- a/secretstores/akeyless/metadata.yaml +++ b/secretstores/akeyless/metadata.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=../../../component-metadata-schema.json +# yaml-language-server: $schema=../../component-metadata-schema.json schemaVersion: v1 type: secretstores name: akeyless @@ -12,12 +12,27 @@ metadata: - name: gatewayUrl required: false description: | - The Akeyless Gateway URL. If not provided, uses the default Akeyless cloud URL. + The URL to the Akeyless Gateway API. Default is https://api.akeyless.io. + default: "https://api.akeyless.io" example: "https://your.akeyless.gw" type: string - - name: token + - name: accessId required: true description: | - The Akeyless authentication token (t-token). - example: "t-ABCD1234" + The Akeyless Access ID. Currently supported authentication methods are: API keys (`access_key`, default), JWT (`jwt`) and AWS IAM (`aws_iam`). + example: "p-123456780wm" type: string + - name: jwt + required: false + description: | + If using the JWT authentication method, specify it here. + example: "eyJ..." + type: string + sensitive: true + - name: accessKey + required: false + description: | + If using the API key (access_key) authentication method, specify it here. + example: "ABCD1233...=" + type: string + sensitive: true \ No newline at end of file diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go new file mode 100644 index 0000000000..956dff7055 --- /dev/null +++ b/secretstores/akeyless/utils.go @@ -0,0 +1,133 @@ +package akeyless + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + + aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + "github.com/akeylesslabs/akeyless-go/v5" +) + +// Define constants for the access types. These are equivalent to the TypeScript consts. +const ( + AKEYLESS_AUTH_ACCESS_JWT = "jwt" + AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE = "access_key" + AKEYLESS_AUTH_ACCESS_IAM = "aws_iam" + AKEYLESS_PUBLIC_GATEWAY_URL = "https://api.akeyless.io" + AKEYLESS_USER_AGENT = "dapr.io/akeyless-secret-store" +) + +// AccessTypeCharMap maps single-character access types to their display names. +var AccessTypeCharMap = map[string]string{ + "a": AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE, + "o": AKEYLESS_AUTH_ACCESS_JWT, + "w": AKEYLESS_AUTH_ACCESS_IAM, +} + +// AccessIdRegex is the compiled regular expression for validating Akeyless Access IDs. +var AccessIdRegex = regexp.MustCompile(`^p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})$`) + +// isValidAccessIdFormat validates the format of an Akeyless Access ID. +// The format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12}). +// It returns true if the format is valid, and false otherwise. +func IsValidAccessIdFormat(accessId string) bool { + return AccessIdRegex.MatchString(accessId) +} + +// extractAccessTypeChar extracts the Akeyless Access Type character from a valid Access ID. +// The access type character is the second to last character of the ID part. +// It returns the single-character access type (e.g., 'a', 'o') or an empty string and an error if the format is invalid. +func ExtractAccessTypeChar(accessId string) (string, error) { + if !IsValidAccessIdFormat(accessId) { + return "", errors.New("invalid access ID format") + } + parts := strings.Split(accessId, "-") + idPart := parts[1] // Get the part after "p-" + // The access type char is the second-to-last character + return string(idPart[len(idPart)-2]), nil +} + +// validateAccessTypeChar validates the extracted access type character against a list of allowed types. +// It returns true if the extracted access type is in the allowed list, false otherwise. +func ValidateAccessTypeChar(accessId string, allowedTypes []string) bool { + typeChar, err := ExtractAccessTypeChar(accessId) + if err != nil { + return false // Invalid ID format + } + + for _, allowedType := range allowedTypes { + if typeChar == allowedType { + return true + } + } + return false +} + +// getAccessTypeDisplayName gets the full display name of the access type from the character. +// It returns the display name (e.g., 'api_key') or an error if the type character is unknown. +func GetAccessTypeDisplayName(typeChar string) (string, error) { + if typeChar == "" { + return "", errors.New("unable to retrieve access type, missing type char") + } + displayName, ok := AccessTypeCharMap[typeChar] + if !ok { + return "Unknown", errors.New("access type character not found in map") + } + return displayName, nil +} + +// Authenticate authenticates with Akeyless using the provided metadata. +// It returns an error if the authentication fails. +func Authenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + + akeylessSecretStore.logger.Debug("Creating authentication request to Akeyless...") + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + authRequest.SetAccessType(metadata.AccessType) + + // Depending on the access type we set the appropriate authentication method + switch metadata.AccessType { + // If access type is AWS IAM we use the cloud ID + case AKEYLESS_AUTH_ACCESS_IAM: + akeylessSecretStore.logger.Debug("Getting cloud ID for AWS IAM...") + id, err := aws.GetCloudId() + if err != nil { + return errors.New("unable to get cloud ID") + } + authRequest.SetCloudId(id) + case AKEYLESS_AUTH_ACCESS_JWT: + akeylessSecretStore.logger.Debug("Setting JWT for authentication...") + authRequest.SetJwt(metadata.JWT) + case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + akeylessSecretStore.logger.Debug("Setting access key for authentication...") + authRequest.SetAccessKey(metadata.AccessKey) + } + + // Create Akeyless API client configuration + akeylessSecretStore.logger.Debug("Creating Akeyless API client configuration...") + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = AKEYLESS_USER_AGENT + config.AddDefaultHeader("akeylessclienttype", AKEYLESS_USER_AGENT) + + akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api + + akeylessSecretStore.logger.Debug("Authenticating with Akeyless...") + out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() + if err != nil { + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + + akeylessSecretStore.logger.Debug("Setting token %s for authentication...", out.GetToken()[:5]+"[REDACTED]") + akeylessSecretStore.logger.Debug("Expires at: %s", out.GetExpiration()) + akeylessSecretStore.token = out.GetToken() + + return nil +} From b1bf217520ed0b577f40f98f7bd1fdd24b2c2890 Mon Sep 17 00:00:00 2001 From: Cassie Coyle Date: Mon, 22 Sep 2025 09:51:21 -0500 Subject: [PATCH 03/28] cleanup daprbot (#4033) Signed-off-by: Cassandra Coyle Signed-off-by: Kobbi Gal --- .github/scripts/dapr_bot.js | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/.github/scripts/dapr_bot.js b/.github/scripts/dapr_bot.js index 328972d27e..5d1c958abf 100644 --- a/.github/scripts/dapr_bot.js +++ b/.github/scripts/dapr_bot.js @@ -1,32 +1,18 @@ // list of owner who can control dapr-bot workflow // TODO: Read owners from OWNERS file. const owners = [ - 'addjuarez', - 'amuluyavarote', - 'artursouza', + 'acroca', 'berndverst', + 'cicoyle', 'daixiang0', - 'DeepanshuA', - 'elena-kolevska', - 'halspang', - 'ItalyPaleAle', + 'javier-aliaga', 'jjcollinge', 'joshvanl', 'mikeee', 'msfussell', - 'mukundansundar', - 'pkedy', - 'pravinpushkar', - 'robertojrojas', - 'RyanLettieri', - 'shivamkm07', - 'shubham1172', + 'nelson-parente', 'sicoyle', - 'skyao', - 'Taction', - 'tmacam', 'yaron2', - 'yash-nisar', ] const docsIssueBodyTpl = ( From 7ab853b17139f754fc0049bbe70e066357d9b2d0 Mon Sep 17 00:00:00 2001 From: Javier Aliaga Date: Tue, 23 Sep 2025 19:04:21 +0200 Subject: [PATCH 04/28] [1.17] Close file after write (#4034) Signed-off-by: Javier Aliaga Co-authored-by: Sam Signed-off-by: Kobbi Gal --- bindings/sftp/sftp.go | 7 +++ bindings/sftp/sftp_integration_test.go | 82 ++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 bindings/sftp/sftp_integration_test.go diff --git a/bindings/sftp/sftp.go b/bindings/sftp/sftp.go index 960294a44a..211cce85d3 100644 --- a/bindings/sftp/sftp.go +++ b/bindings/sftp/sftp.go @@ -173,6 +173,13 @@ func (sftp *Sftp) create(_ context.Context, req *bindings.InvokeRequest) (*bindi return nil, fmt.Errorf("sftp binding error: error create file %s: %w", path, err) } + defer func() { + closeErr := file.Close() + if closeErr != nil { + sftp.logger.Errorf("sftp binding error: error close file: %w", closeErr) + } + }() + _, err = file.Write(req.Data) if err != nil { return nil, fmt.Errorf("sftp binding error: error write file: %w", err) diff --git a/bindings/sftp/sftp_integration_test.go b/bindings/sftp/sftp_integration_test.go new file mode 100644 index 0000000000..178c7535db --- /dev/null +++ b/bindings/sftp/sftp_integration_test.go @@ -0,0 +1,82 @@ +package sftp + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/dapr/components-contrib/bindings" +) + +var connectionStringEnvKey = "DAPR_TEST_SFTP_CONNSTRING" + +// Run docker from the file location as the upload folder is relative to the test +// docker run -v ./upload:/home/foo/upload -p 2222:22 -d atmoz/sftp foo:pass:1001 +func TestIntegrationCases(t *testing.T) { + connectionString := os.Getenv(connectionStringEnvKey) + if connectionString == "" { + t.Skipf(`sftp binding integration tests skipped. To enable this test, define the connection string using environment variable '%[1]s' (example 'export %[1]s="localhost:2222")'`, connectionStringEnvKey) + } + + t.Run("List operation", testListOperation) + t.Run("Create operation", testCreateOperation) +} + +func testListOperation(t *testing.T) { + c := Sftp{} + m := bindings.Metadata{} + m.Properties = map[string]string{ + "rootPath": "/upload", + "address": os.Getenv(connectionStringEnvKey), + "username": "foo", + "password": "pass", + "insecureIgnoreHostKey": "true", + } + err := c.Init(t.Context(), m) + require.NoError(t, err) + + r, err := c.Invoke(t.Context(), &bindings.InvokeRequest{Operation: bindings.ListOperation}) + require.NoError(t, err) + assert.NotNil(t, r.Data) + + var d []listResponse + err = json.Unmarshal(r.Data, &d) + require.NoError(t, err) +} + +func testCreateOperation(t *testing.T) { + c := Sftp{} + m := bindings.Metadata{} + m.Properties = map[string]string{ + "rootPath": "/upload", + "address": os.Getenv(connectionStringEnvKey), + "username": "foo", + "password": "pass", + "insecureIgnoreHostKey": "true", + } + + err := os.Remove("./upload/test.txt") + if err != nil && !os.IsNotExist(err) { + require.NoError(t, err) + } + + err = c.Init(t.Context(), m) + require.NoError(t, err) + + r, err := c.Invoke(t.Context(), &bindings.InvokeRequest{ + Operation: bindings.CreateOperation, + Data: []byte("test data 1"), + Metadata: map[string]string{ + "fileName": "test.txt", + }, + }) + require.NoError(t, err) + assert.NotNil(t, r.Data) + + file, err := os.Stat("./upload/test.txt") + require.NoError(t, err) + assert.Equal(t, "test.txt", file.Name()) +} From d43962a2402d4b4547cd28c4cae7cbdcab217f9e Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 24 Sep 2025 17:45:53 -0400 Subject: [PATCH 05/28] retrieve static secrets only Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 18 ++- secretstores/akeyless/akeyless.go | 38 +++-- secretstores/akeyless/akeyless_test.go | 192 +++++++++++++++++++++---- 3 files changed, 196 insertions(+), 52 deletions(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index 042c5447d0..82c9b28b24 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -13,6 +13,12 @@ The Akeyless secret store component supports the following configuration options | `jwt` | No | If using an OAuth2.0/JWT access ID, specify the JSON Web Token | `eyJ...` | | `accessKey` | No | If using an API Key access ID, specify the API key | `ABCD123...=` | +We currently support the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + +- [API Key](https://docs.akeyless.io/docs/api-key) +- [OAuth2.0/JWT](https://docs.akeyless.io/docs/oauth20jwt) +- [AWS IAM](https://docs.akeyless.io/docs/aws-iam) + ## Example Configuration: API Key ```yaml @@ -49,7 +55,7 @@ spec: - name: accessId value: "p-1234Abcdom" - name: jwt - value: "eyJ" + value: "eyJ....." ``` ## Example Configuration: AWS IAM @@ -58,7 +64,7 @@ spec: apiVersion: dapr.io/v1alpha1 kind: Component metadata: - name: akeyless-secretstore + name: akeyless spec: type: secretstores.akeyless version: v1 @@ -75,13 +81,13 @@ Once configured, you can retrieve secrets using the Dapr secrets API: ```bash # Get a single secret -curl http://localhost:3500/v1.0/secrets/akeyless-secretstore/my-secret +curl http://localhost:3500/v1.0/secrets/akeyless/my-secret # Get all secrets -curl http://localhost:3500/v1.0/secrets/akeyless-secretstore/bulk +curl http://localhost:3500/v1.0/secrets/akeyless/bulk ``` ## Features -- **GetSecret**: Retrieve individual secrets by name -- **BulkGetSecret**: Retrieve all secrets from the Akeyless vault +- **GetSecret**: Retrieve an individual static secret by name. +- **BulkGetSecret**: Retrieve an all static secrets. diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index b4cdb0e8a4..0aabb44459 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -110,12 +110,15 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore // For bulk get, we need to list all secrets first listItems := akeyless.NewListItems() listItems.SetToken(a.token) + listItems.SetPath("/") + listItems.SetType([]string{"static-secret"}) // Execute the list items request itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() if err != nil { return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to list items from Akeyless: %w", err) } + a.logger.Debug("%d items (static-secret) returned from Akeyless", len(itemsList.Items)) // Create a map to store all secrets for response allSecrets := make(map[string]map[string]string) @@ -132,28 +135,33 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore // Get all secrets getSecretValue := akeyless.NewGetSecretValue(itemsNames) getSecretValue.SetToken(a.token) - secretResp, _, err := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() + secretResp, httpResp, err := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() if err != nil { - return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to get secrets from Akeyless: %w", err) + return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to get secrets from Akeyless: %w (%d)", err, httpResp.StatusCode) } // Add all secrets to the response for name, secret := range secretResp { - // secret is of type interface{}, need to assert to map[string]interface{} first - secretMap, ok := secret.(map[string]any) - if !ok { - return secretstores.BulkGetSecretResponse{}, fmt.Errorf("unexpected secret type for %s", name) - } - // Convert map[string]interface{} to map[string]string - secretStrMap := make(map[string]string) - for k, v := range secretMap { - if v == nil { - secretStrMap[k] = "" - } else { - secretStrMap[k] = fmt.Sprintf("%v", v) + // secretResp has the following format: + // { + // "/aws_iam_secret": "noSecret", + // "/cache-test": "1234" + // } + // Each secret value is directly the secret content, not a nested map + var secretStr string + if str, ok := secret.(string); ok { + secretStr = str + } else { + // If it's not a string, convert it to JSON string + secretBytes, err := json.Marshal(secret) + if err != nil { + return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to convert secret value to string for %s: %w", name, err) } + secretStr = string(secretBytes) + } + allSecrets[name] = map[string]string{ + name: secretStr, } - allSecrets[name] = secretStrMap } return secretstores.BulkGetSecretResponse{ diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index c921d8dbb7..adaeb464c6 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -38,7 +38,6 @@ const mockCloudID = "123456789012" // mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { - akeylessSecretStore.logger.Debug("Creating authentication request to Akeyless...") authRequest := akeyless.NewAuth() authRequest.SetAccessId(metadata.AccessID) authRequest.SetAccessType(metadata.AccessType) @@ -57,8 +56,6 @@ func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessS authRequest.SetAccessKey(metadata.AccessKey) } - // Create Akeyless API client configuration - akeylessSecretStore.logger.Debug("Creating Akeyless API client configuration...") config := akeyless.NewConfiguration() config.Servers = []akeyless.ServerConfiguration{ { @@ -70,14 +67,11 @@ func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessS akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api - akeylessSecretStore.logger.Debug("Authenticating with Akeyless...") out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() if err != nil { return fmt.Errorf("failed to authenticate with Akeyless: %w", err) } - akeylessSecretStore.logger.Debug("Setting token %s for authentication...", out.GetToken()[:5]+"[REDACTED]") - akeylessSecretStore.logger.Debug("Expires at: %s", out.GetExpiration()) akeylessSecretStore.token = out.GetToken() return nil @@ -102,12 +96,45 @@ func TestMain(m *testing.M) { w.Write(jsonResponse) case "/get-secret-value", "/v2/get-secret-value": // Return a mock secret value response + // For bulk requests, each secret should be a map[string]interface{} secretResponse := map[string]interface{}{ - "my-secret": "secret-value-123", + "my-secret": map[string]interface{}{ + "my-secret": "secret-value-123", + }, + "test-secret": map[string]interface{}{ + "test-secret": "test-value-456", + }, + "json-secret": map[string]interface{}{ + "json-secret": map[string]interface{}{ + "username": "admin", + "password": "secret123", + }, + }, } jsonResponse, _ := json.Marshal(secretResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) + case "/list-items", "/v2/list-items": + // Return a mock list items response + listResponse := map[string]interface{}{ + "items": []map[string]interface{}{ + { + "item_name": "my-secret", + "item_type": "STATIC_SECRET", + }, + { + "item_name": "test-secret", + "item_type": "STATIC_SECRET", + }, + { + "item_name": "json-secret", + "item_type": "STATIC_SECRET", + }, + }, + } + jsonResponse, _ := json.Marshal(listResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) default: // Default response for any other endpoint w.WriteHeader(http.StatusOK) @@ -225,30 +252,6 @@ func TestInit(t *testing.T) { } } -func TestGetSecretWithoutInit(t *testing.T) { - log := logger.NewLogger("test") - store := NewAkeylessSecretStore(log).(*akeylessSecretStore) - - req := secretstores.GetSecretRequest{ - Name: "test-secret", - } - - _, err := store.GetSecret(context.Background(), req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not initialized") -} - -func TestBulkGetSecretWithoutInit(t *testing.T) { - log := logger.NewLogger("test") - store := NewAkeylessSecretStore(log).(*akeylessSecretStore) - - req := secretstores.BulkGetSecretRequest{} - - _, err := store.BulkGetSecret(context.Background(), req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not initialized") -} - func TestFeatures(t *testing.T) { log := logger.NewLogger("test") store := NewAkeylessSecretStore(log) @@ -414,3 +417,130 @@ func TestMockAWSCloudID(t *testing.T) { assert.NotNil(t, store.token) assert.Equal(t, "t-1234567890", store.token) } + +func TestGetSecret(t *testing.T) { + // Setup a properly initialized store + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + tests := []struct { + name string + request secretstores.GetSecretRequest + expectError bool + expectedSecret string + }{ + { + name: "get existing string secret", + request: secretstores.GetSecretRequest{ + Name: "my-secret", + }, + expectError: false, + expectedSecret: `{"my-secret":"secret-value-123"}`, + }, + { + name: "get existing test secret", + request: secretstores.GetSecretRequest{ + Name: "test-secret", + }, + expectError: false, + expectedSecret: `{"test-secret":"test-value-456"}`, + }, + { + name: "get non-existing secret", + request: secretstores.GetSecretRequest{ + Name: "non-existing-secret", + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + response, err := store.GetSecret(context.Background(), tt.request) + if tt.expectError { + assert.Error(t, err) + assert.Empty(t, response.Data) + } else { + assert.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, tt.request.Name) + assert.Equal(t, tt.expectedSecret, response.Data[tt.request.Name]) + } + }) + } +} + +func TestGetSecretWithoutInit(t *testing.T) { + // Test GetSecret without initialization + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + req := secretstores.GetSecretRequest{ + Name: "test-secret", + } + + _, err := store.GetSecret(context.Background(), req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} + +func TestBulkGetSecret(t *testing.T) { + // Setup a properly initialized store + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + // Test bulk get secret + req := secretstores.BulkGetSecretRequest{} + response, err := store.BulkGetSecret(context.Background(), req) + + assert.NoError(t, err) + assert.NotNil(t, response.Data) + + // Check that we got the expected secrets + expectedSecrets := []string{"my-secret", "test-secret", "json-secret"} + for _, secretName := range expectedSecrets { + assert.Contains(t, response.Data, secretName) + } + + // Check specific secret values + assert.Equal(t, "{\"my-secret\":\"secret-value-123\"}", response.Data["my-secret"]["my-secret"]) + assert.Equal(t, "{\"test-secret\":\"test-value-456\"}", response.Data["test-secret"]["test-secret"]) + + // Check JSON secret (should be converted to string) + jsonSecret := response.Data["json-secret"]["json-secret"] + assert.Contains(t, jsonSecret, "username") + assert.Contains(t, jsonSecret, "admin") + assert.Contains(t, jsonSecret, "password") + assert.Contains(t, jsonSecret, "secret123") +} + +func TestBulkGetSecretWithoutInit(t *testing.T) { + // Test BulkGetSecret without initialization + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + + req := secretstores.BulkGetSecretRequest{} + _, err := store.BulkGetSecret(context.Background(), req) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not initialized") +} From 046f2cc0544920743c0c5dcc446b842ecbe73863 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 26 Sep 2025 17:14:17 -0400 Subject: [PATCH 06/28] cleaned up tests added support for single static secret value Signed-off-by: Kobbi Gal --- go.mod | 2 +- secretstores/akeyless/akeyless.go | 92 +---- secretstores/akeyless/akeyless_test.go | 277 ++++++++++----- secretstores/akeyless/utils.go | 472 ++++++++++++++++++++++++- 4 files changed, 654 insertions(+), 189 deletions(-) diff --git a/go.mod b/go.mod index 7f17d46859..aa00f35cc6 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/IBM/sarama v1.45.2 github.com/aerospike/aerospike-client-go/v6 v6.12.0 + github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 github.com/akeylesslabs/akeyless-go/v5 v5.0.8 github.com/alibaba/sentinel-golang v1.0.4 github.com/alibabacloud-go/darabonba-openapi v0.2.1 @@ -178,7 +179,6 @@ require ( github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect github.com/ajg/form v1.5.1 // indirect - github.com/akeylesslabs/akeyless-go-cloud-id v0.3.5 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 0aabb44459..73caf3f8ee 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -2,7 +2,6 @@ package akeyless import ( "context" - "encoding/json" "errors" "fmt" "reflect" @@ -64,41 +63,22 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") } - // Create the get secret value request - getSecretValue := akeyless.NewGetSecretValue([]string{req.Name}) - getSecretValue.SetToken(a.token) - - // Execute the request - result, _, err := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() + a.logger.Debug("getting secret type for '%s'...", req.Name) + secretType, err := GetSecretType(req.Name, a) if err != nil { - return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get secret from Akeyless: %w", err) + return secretstores.GetSecretResponse{}, err } - // Extract the secret value - secretValue, exists := result[req.Name] - if !exists { - return secretstores.GetSecretResponse{}, fmt.Errorf("secret '%s' not found", req.Name) - } + a.logger.Debug("getting secret value for '%s' (type %s)...", req.Name, secretType) - // Convert the secret value to string - var secretStr string - if str, ok := secretValue.(string); ok { - secretStr = str - } else { - // If it's not a string, convert it to JSON string - secretBytes, err := json.Marshal(secretValue) - if err != nil { - return secretstores.GetSecretResponse{}, fmt.Errorf("failed to convert secret value to string: %w", err) - } - secretStr = string(secretBytes) + secretValue, err := GetSingleSecretValue(req.Name, secretType, a) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New(err.Error()) } + a.logger.Debug("secret '%s' value: %s", req.Name, secretValue[:3]+"[REDACTED]") // Return the secret in the expected format - return secretstores.GetSecretResponse{ - Data: map[string]string{ - req.Name: secretStr, - }, - }, nil + return GetDaprSingleSecretResponse(req.Name, secretValue) } // BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. @@ -111,62 +91,18 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore listItems := akeyless.NewListItems() listItems.SetToken(a.token) listItems.SetPath("/") - listItems.SetType([]string{"static-secret"}) + listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) // Execute the list items request itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() if err != nil { return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to list items from Akeyless: %w", err) } - a.logger.Debug("%d items (static-secret) returned from Akeyless", len(itemsList.Items)) - - // Create a map to store all secrets for response - allSecrets := make(map[string]map[string]string) - - // Create a list of item names from all items - itemsNames := make([]string, 0, len(itemsList.Items)) - for _, item := range itemsList.Items { - if item.ItemName == nil { - continue - } - itemsNames = append(itemsNames, *item.ItemName) - } - - // Get all secrets - getSecretValue := akeyless.NewGetSecretValue(itemsNames) - getSecretValue.SetToken(a.token) - secretResp, httpResp, err := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() - if err != nil { - return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to get secrets from Akeyless: %w (%d)", err, httpResp.StatusCode) - } - - // Add all secrets to the response - for name, secret := range secretResp { - // secretResp has the following format: - // { - // "/aws_iam_secret": "noSecret", - // "/cache-test": "1234" - // } - // Each secret value is directly the secret content, not a nested map - var secretStr string - if str, ok := secret.(string); ok { - secretStr = str - } else { - // If it's not a string, convert it to JSON string - secretBytes, err := json.Marshal(secret) - if err != nil { - return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to convert secret value to string for %s: %w", name, err) - } - secretStr = string(secretBytes) - } - allSecrets[name] = map[string]string{ - name: secretStr, - } - } + a.logger.Debug("%d items returned from Akeyless", len(itemsList.Items)) - return secretstores.BulkGetSecretResponse{ - Data: allSecrets, - }, nil + // Use the new BulkGetSecretResponse function to handle all secret types properly + // return BulkGetSecretResponse(ctx, itemsList.Items, a) + return secretstores.BulkGetSecretResponse{}, nil } // Features returns the features available in this secret store. diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index adaeb464c6..ec843a5992 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -28,8 +28,75 @@ const ( // "iat": 1516239022 // } testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" + // testJSONStaticSecretName = "/path/to/json-static-secret" + // testDynamicSecretName = "/path/to/dynamic-secret-test" + // testRotatedSecretName = "/path/to/rotated-secret-test" + testSecretValue = "r3vE4L3D" ) +// Mock responses for GetSecret and BulkGetSecret +// var testJSONSecretValue = map[string]string{ +// "some": "json", +// } + +var ( + mockDescribeStaticSecretName = "/path/to/akeyless/static-secret-test" + mockDescribeStaticSecretType = AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE + mockDescribeStaticSecretItemResponse = akeyless.Item{ + ItemName: &mockDescribeStaticSecretName, + ItemType: &mockDescribeStaticSecretType, + } +) + +// var mockDescribeRotatedSecretItemResponse = akeyless.Item{ +// "item_name": testRotatedSecretName, +// "item_type": AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE, +// } + +var mockGetSingleSecretValueResponse = map[string]string{ + mockDescribeStaticSecretName: testSecretValue, +} + +// var mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ +// testJSONStaticSecretName: testJSONSecretValue, +// } + +// var mockGetBulkSecretValueResponse = map[string]any{ +// testStaticSecretName: testSecretValue + "1", +// testStaticSecretName + "2": testSecretValue + "2", +// testJSONStaticSecretName: testJSONSecretValue, +// } + +// var mockListItemsResponse = map[string]interface{}{ +// "items": []map[string]string{ +// mockDescribeStaticSecretItemResponse, +// mockDescribeDynamicSecretItemResponse, +// mockDescribeRotatedSecretItemResponse, +// }, +// } + +// var mockGetDynamicSecretValueResponse = map[string]any{ +// "id": "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", +// "msg": "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", +// "secret": map[string]any{ +// "appId": "1234567890", +// "displayName": "tmp.p-1234567890.GV7LR", +// "endDateTime": "2025-09-26T14:54:05.1643791Z", +// "keyId": "1234567890", +// "secretText": testSecretValue, +// "tenantId": "1234567890", +// }, +// "ttl_in_minutes": "60", +// } + +// var mockGetRotatedSecretValueResponse = map[string]any{ +// "value": map[string]string{ +// "username": "abcdefghijklmnopqrstuvwxyz", +// "password": testSecretValue, +// "application_id": "1234567890", +// }, +// } + // Global mock server for all tests var mockGateway *httptest.Server @@ -87,52 +154,55 @@ func TestMain(m *testing.M) { switch r.URL.Path { case "/auth", "/v2/auth": // Return a proper AuthOutput JSON response for authentication - authOutput := map[string]interface{}{ - "token": "t-1234567890", - "expiration": "2025-01-01T00:00:00Z", - } + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) + // Single static secret value case "/get-secret-value", "/v2/get-secret-value": - // Return a mock secret value response - // For bulk requests, each secret should be a map[string]interface{} - secretResponse := map[string]interface{}{ - "my-secret": map[string]interface{}{ - "my-secret": "secret-value-123", - }, - "test-secret": map[string]interface{}{ - "test-secret": "test-value-456", - }, - "json-secret": map[string]interface{}{ - "json-secret": map[string]interface{}{ - "username": "admin", - "password": "secret123", - }, - }, - } - jsonResponse, _ := json.Marshal(secretResponse) + jsonResponse, _ := json.Marshal(mockGetSingleSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) + // case "/get-dynamic-secret-value", "/v2/get-dynamic-secret-value": + // var dynamicResponse = DynamicSecretResponse{ + // ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", + // Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", + // Secret: DynamicSecretSecret{ + // AppID: "1234567890", + // DisplayName: "tmp.p-1234567890.GV7LR", + // EndDateTime: "2025-09-26T14:54:05.1643791Z", + // KeyID: "1234567890", + // SecretText: testSecretValue, + // TenantID: "1234567890", + // }, + // TTLInMinutes: "60", + // } + // jsonResponse, _ := json.Marshal(dynamicResponse) + // w.WriteHeader(http.StatusOK) + // w.Write(jsonResponse) + // case "/get-rotated-secret-value", "/v2/get-rotated-secret-value": + // var rotatedResponse = RotatedSecretResponse{ + // Value: RotatedSecretValue{ + // Username: "abcdefghijklmnopqrstuvwxyz", + // Password: testSecretValue, + // ApplicationID: "1234567890", + // }, + // } + // jsonResponse, _ := json.Marshal(rotatedResponse) + // w.WriteHeader(http.StatusOK) + // w.Write(jsonResponse) case "/list-items", "/v2/list-items": - // Return a mock list items response - listResponse := map[string]interface{}{ - "items": []map[string]interface{}{ - { - "item_name": "my-secret", - "item_type": "STATIC_SECRET", - }, - { - "item_name": "test-secret", - "item_type": "STATIC_SECRET", - }, - { - "item_name": "json-secret", - "item_type": "STATIC_SECRET", - }, - }, - } - jsonResponse, _ := json.Marshal(listResponse) + listItemsResponse := akeyless.NewListItemsInPathOutput() + listItemsResponse.SetItems( + []akeyless.Item{mockDescribeStaticSecretItemResponse}, + ) + jsonResponse, _ := json.Marshal(listItemsResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item", "/v2/describe-item": + jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) default: @@ -441,28 +511,30 @@ func TestGetSecret(t *testing.T) { expectedSecret string }{ { - name: "get existing string secret", + name: "test text single static secret", request: secretstores.GetSecretRequest{ - Name: "my-secret", + Name: mockDescribeStaticSecretName, }, expectError: false, - expectedSecret: `{"my-secret":"secret-value-123"}`, + expectedSecret: testSecretValue, }, { name: "get existing test secret", request: secretstores.GetSecretRequest{ - Name: "test-secret", + Name: mockDescribeStaticSecretName, }, expectError: false, - expectedSecret: `{"test-secret":"test-value-456"}`, - }, - { - name: "get non-existing secret", - request: secretstores.GetSecretRequest{ - Name: "non-existing-secret", - }, - expectError: true, + expectedSecret: testSecretValue, }, + // TODO: add non-existing secret test + // { + // name: "get non-existing secret", + // request: secretstores.GetSecretRequest{ + // Name: mockDescribeStaticSecretName, + // }, + // expectError: true, + // expectedSecret: "", + // }, } for _, tt := range tests { @@ -481,21 +553,60 @@ func TestGetSecret(t *testing.T) { } } -func TestGetSecretWithoutInit(t *testing.T) { - // Test GetSecret without initialization - store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) - - req := secretstores.GetSecretRequest{ - Name: "test-secret", - } - - _, err := store.GetSecret(context.Background(), req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not initialized") -} - -func TestBulkGetSecret(t *testing.T) { - // Setup a properly initialized store +// func TestBulkGetSecret(t *testing.T) { +// // Setup a properly initialized store +// store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) +// meta := secretstores.Metadata{ +// Base: metadata.Base{ +// Properties: map[string]string{ +// "accessId": testAccessIdKey, +// "accessKey": testAccessKey, +// "gatewayUrl": mockGateway.URL, +// }, +// }, +// } + +// err := store.Init(context.Background(), meta) +// require.NoError(t, err) + +// // Test bulk get secret +// req := secretstores.BulkGetSecretRequest{} +// response, err := store.BulkGetSecret(context.Background(), req) + +// assert.NoError(t, err) +// assert.NotNil(t, response.Data) + +// // Check that we got the expected secrets +// expectedSecrets := []string{"my-secret", "test-secret", "json-secret"} +// for _, secretName := range expectedSecrets { +// assert.Contains(t, response.Data, secretName) +// } + +// // Check specific secret values +// assert.Equal(t, "{\"my-secret\":\"secret-value-123\"}", response.Data["my-secret"]["my-secret"]) +// assert.Equal(t, "{\"test-secret\":\"test-value-456\"}", response.Data["test-secret"]["test-secret"]) + +// // Check JSON secret (should be converted to string) +// jsonSecret := response.Data["json-secret"]["json-secret"] +// assert.Contains(t, jsonSecret, "username") +// assert.Contains(t, jsonSecret, "admin") +// assert.Contains(t, jsonSecret, "password") +// assert.Contains(t, jsonSecret, "secret123") +// } + +// func TestBulkGetSecretWithoutInit(t *testing.T) { +// // Test BulkGetSecret without initialization +// store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + +// req := secretstores.BulkGetSecretRequest{} +// _, err := store.BulkGetSecret(context.Background(), req) +// assert.Error(t, err) +// assert.Contains(t, err.Error(), "not initialized") +// } + +// Test GetSecretType functions +func TestGetSecretType(t *testing.T) { + // Test GetSecretType store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) meta := secretstores.Metadata{ Base: metadata.Base{ @@ -510,37 +621,7 @@ func TestBulkGetSecret(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) - // Test bulk get secret - req := secretstores.BulkGetSecretRequest{} - response, err := store.BulkGetSecret(context.Background(), req) - + secretType, err := GetSecretType(mockDescribeStaticSecretName, store) assert.NoError(t, err) - assert.NotNil(t, response.Data) - - // Check that we got the expected secrets - expectedSecrets := []string{"my-secret", "test-secret", "json-secret"} - for _, secretName := range expectedSecrets { - assert.Contains(t, response.Data, secretName) - } - - // Check specific secret values - assert.Equal(t, "{\"my-secret\":\"secret-value-123\"}", response.Data["my-secret"]["my-secret"]) - assert.Equal(t, "{\"test-secret\":\"test-value-456\"}", response.Data["test-secret"]["test-secret"]) - - // Check JSON secret (should be converted to string) - jsonSecret := response.Data["json-secret"]["json-secret"] - assert.Contains(t, jsonSecret, "username") - assert.Contains(t, jsonSecret, "admin") - assert.Contains(t, jsonSecret, "password") - assert.Contains(t, jsonSecret, "secret123") -} - -func TestBulkGetSecretWithoutInit(t *testing.T) { - // Test BulkGetSecret without initialization - store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) - - req := secretstores.BulkGetSecretRequest{} - _, err := store.BulkGetSecret(context.Background(), req) - assert.Error(t, err) - assert.Contains(t, err.Error(), "not initialized") + assert.Equal(t, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE, secretType) } diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 956dff7055..2d4b16d356 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -7,17 +7,26 @@ import ( "regexp" "strings" + "encoding/json" + aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" "github.com/akeylesslabs/akeyless-go/v5" + "github.com/dapr/components-contrib/secretstores" ) // Define constants for the access types. These are equivalent to the TypeScript consts. const ( - AKEYLESS_AUTH_ACCESS_JWT = "jwt" - AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE = "access_key" - AKEYLESS_AUTH_ACCESS_IAM = "aws_iam" - AKEYLESS_PUBLIC_GATEWAY_URL = "https://api.akeyless.io" - AKEYLESS_USER_AGENT = "dapr.io/akeyless-secret-store" + AKEYLESS_AUTH_ACCESS_JWT = "jwt" + AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE = "access_key" + AKEYLESS_AUTH_ACCESS_IAM = "aws_iam" + AKEYLESS_PUBLIC_GATEWAY_URL = "https://api.akeyless.io" + AKEYLESS_USER_AGENT = "dapr.io/akeyless-secret-store" + AKEYLESS_SECRET_TYPE_STATIC = "static-secret" + AKEYLESS_SECRET_TYPE_DYNAMIC = "dynamic-secret" + AKEYLESS_SECRET_TYPE_ROTATED = "rotated-secret" + AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE = "STATIC_SECRET" + AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE = "DYNAMIC_SECRET" + AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE = "ROTATED_SECRET" ) // AccessTypeCharMap maps single-character access types to their display names. @@ -92,22 +101,22 @@ func Authenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecre switch metadata.AccessType { // If access type is AWS IAM we use the cloud ID case AKEYLESS_AUTH_ACCESS_IAM: - akeylessSecretStore.logger.Debug("Getting cloud ID for AWS IAM...") + akeylessSecretStore.logger.Debug("getting cloud ID for AWS IAM...") id, err := aws.GetCloudId() if err != nil { return errors.New("unable to get cloud ID") } authRequest.SetCloudId(id) case AKEYLESS_AUTH_ACCESS_JWT: - akeylessSecretStore.logger.Debug("Setting JWT for authentication...") + akeylessSecretStore.logger.Debug("setting JWT for authentication...") authRequest.SetJwt(metadata.JWT) case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: - akeylessSecretStore.logger.Debug("Setting access key for authentication...") + akeylessSecretStore.logger.Debug("setting access key for authentication...") authRequest.SetAccessKey(metadata.AccessKey) } // Create Akeyless API client configuration - akeylessSecretStore.logger.Debug("Creating Akeyless API client configuration...") + akeylessSecretStore.logger.Debug("creating Akeyless API client configuration...") config := akeyless.NewConfiguration() config.Servers = []akeyless.ServerConfiguration{ { @@ -119,15 +128,454 @@ func Authenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecre akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api - akeylessSecretStore.logger.Debug("Authenticating with Akeyless...") + akeylessSecretStore.logger.Debug("authenticating with Akeyless...") out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() if err != nil { return fmt.Errorf("failed to authenticate with Akeyless: %w", err) } - akeylessSecretStore.logger.Debug("Setting token %s for authentication...", out.GetToken()[:5]+"[REDACTED]") - akeylessSecretStore.logger.Debug("Expires at: %s", out.GetExpiration()) + akeylessSecretStore.logger.Debug("setting token %s for authentication...", out.GetToken()[:3]+"[REDACTED]") + akeylessSecretStore.logger.Debug("expires at: %s", out.GetExpiration()) akeylessSecretStore.token = out.GetToken() return nil } + +// getSecretType gets the type of the secret from the describe item response. +// It returns the type of the secret (e.g. static, dynamic, rotated) or an error if the type is unknown. +func GetSecretType(secretName string, akeylessSecretStore *akeylessSecretStore) (string, error) { + + describeItem := akeyless.NewDescribeItem(secretName) + describeItem.SetToken(akeylessSecretStore.token) + describeItemResp, _, err := akeylessSecretStore.v2.DescribeItem(context.Background()).Body(*describeItem).Execute() + if err != nil { + return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) + } + + if describeItemResp.ItemType == nil { + return "", errors.New("unable to retrieve secret type, missing type in describe item response") + } + + return *describeItemResp.ItemType, nil +} + +func GetSingleSecretValue(secretName string, secretType string, akeylessSecretStore *akeylessSecretStore) (string, error) { + + switch secretType { + case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) + getSecretValue.SetToken(akeylessSecretStore.token) + secretRespMap, _, err := akeylessSecretStore.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() + if err != nil { + return "", fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, err) + } + + typedSecretResp, ok := secretRespMap[secretName].(string) + if !ok { + return "", fmt.Errorf("failed to assert type of secret response to SingleStaticSecretResponse: %w", err) + } + + return typedSecretResp, nil + // TODO implement dynamic secrets + case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + return "", errors.New("dynamic secrets are not supported") + // TODO implement rotated secrets + case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + return "", errors.New("rotated secrets are not supported") + } + + return "", nil +} + +// GetSecretValueByType gets the secret value by the type of the secret. +// It returns the secret value or an error if the secret value is not found. +// If secretName is not a string it means that we're getting numerous static secrets +// and we can get them all at once +func GetSecretValueByType(secretName any, secretType string, akeylessSecretStore *akeylessSecretStore) (secretstores.GetSecretResponse, error) { + var secretResp secretstores.GetSecretResponse + switch secretType { + case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + var secrets []string + switch secretName := secretName.(type) { + case string: + secrets = []string{secretName} + case []string: + secrets = secretName + } + getSecretValue := akeyless.NewGetSecretValue(secrets) + getSecretValue.SetToken(akeylessSecretStore.token) + secretRespMap, _, err := akeylessSecretStore.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() + if err != nil { + return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName.(string), err) + } + + transformedResp, err := transformStaticSecretResponse(secretRespMap) + if err != nil { + return secretstores.GetSecretResponse{}, fmt.Errorf("failed to transform static secret response for secret '%s': %w", secretName.(string), err) + } + secretResp = transformedResp + // case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + // getSecretValue := akeyless.NewGetDynamicSecretValue(secretName.(string)) + // getSecretValue.SetToken(akeylessSecretStore.token) + // secretRespMap, _, err := akeylessSecretStore.v2.GetDynamicSecretValue(context.Background()).Body(*getSecretValue).Execute() + + // if err != nil { + // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName.(string), err) + // } + // // Convert secretRespMap (map[string]interface{}) to DynamicSecretResponse using a helper function + // dynamicSecretResp, err := transformDynamicSecretInterface(secretRespMap) + // if err != nil { + // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to transform dynamic secret response for secret '%s': %w", secretName.(string), err) + // } + + // transformedResp := transformDynamicSecretResponse(secretName.(string), dynamicSecretResp) + // secretResp = transformedResp + // case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + // getSecretValue := akeyless.NewGetRotatedSecretValue(secretName.(string)) + // getSecretValue.SetToken(akeylessSecretStore.token) + // secretRespMap, _, err := akeylessSecretStore.v2.GetRotatedSecretValue(context.Background()).Body(*getSecretValue).Execute() + // if err != nil { + // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName.(string), err) + // } + + // // assert type of secretResp to RotatedSecretResponse + // rotatedSecretResp, ok := secretRespMap.(RotatedSecretResponse) + // if !ok { + // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to assert type of secret response to RotatedSecretResponse: %w", err) + // } + + // // transform the response + // transformedResp := transformRotatedSecretResponse(secretName.(string), rotatedSecretResp) + // secretResp = transformedResp + default: + return secretstores.GetSecretResponse{}, errors.New("unsupported secret type") + } + return secretResp, nil +} + +// func transformDynamicSecretInterface(secretRespMap map[string]interface{}) (DynamicSecretResponse, error) { +// dynamicSecretResp := DynamicSecretResponse{} +// // Attempt to map fields from secretRespMap to dynamicSecretResp +// // This assumes DynamicSecretResponse is a struct and secretRespMap is a map[string]interface{} +// // Use mapstructure or manual assignment as needed +// err := mapstructure.Decode(secretRespMap, &dynamicSecretResp) +// if err != nil { +// return DynamicSecretResponse{}, fmt.Errorf("failed to decode secret response to DynamicSecretResponse: %w", err) +// } +// return dynamicSecretResp, nil +// } + +// convertSecretToString converts the secret to a string. +// It returns the secret value or an error if the secret value is not found. +// func convertSecretResponseToString(secret any) (string, error) { + +// // If the secret is a string, return it +// if secretStr, ok := secret.(string); ok { +// return secretStr, nil +// } + +// // If the secret is a map, marshal it to a JSON string +// secretValueBytes, err := json.Marshal(secret) +// if err != nil { +// return "", fmt.Errorf("failed to marshal secret response: %w", err) +// } + +// // Return the JSON string +// return string(secretValueBytes), nil +// } + +// result is a helper struct to pass results and errors back from goroutines +// type result struct { +// key string +// value *secretstores.GetSecretResponse +// error error +// } + +// BulkGetSecretResponse takes in a list of `akeyless.Item`, +// splits them into static and non-static subsets +// sends an async request to the appropriate API endpoint for each subset +// and returns them as the corresponding `secretstores.BulkGetSecretResponse` +// func BulkGetSecretResponse(ctx context.Context, allSecrets []akeyless.Item, akeylessSecretStore *akeylessSecretStore) (secretstores.BulkGetSecretResponse, error) { +// // Split secrets into static and non-static +// staticSecrets, nonStaticSecrets, err := splitSecretsByType(allSecrets) +// if err != nil { +// return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to split secrets by type: %w", err) +// } + +// akeylessSecretStore.logger.Debug("%d static secrets, %d non-static secrets", len(staticSecrets), len(nonStaticSecrets)) + +// // Process static secrets in batch +// staticResults, err := processStaticSecrets(staticSecrets, akeylessSecretStore) +// if err != nil { +// return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to process static secrets: %w", err) +// } + +// // Process non-static secrets asynchronously +// nonStaticResults, err := processNonStaticSecrets(ctx, nonStaticSecrets, akeylessSecretStore) +// if err != nil { +// return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to process non-static secrets: %w", err) +// } + +// // Combine all results +// allResults := make(map[string]map[string]string) +// maps.Copy(allResults, staticResults) +// maps.Copy(allResults, nonStaticResults) + +// return secretstores.BulkGetSecretResponse{ +// Data: allResults, +// }, nil +// } + +// splitSecretsByType splits secrets into static and non-static (dynamic, rotated) secrets +// func splitSecretsByType(allSecrets []akeyless.Item) ([]string, []akeyless.Item, error) { +// var staticSecrets []string +// var nonStaticSecrets []akeyless.Item + +// for _, secret := range allSecrets { +// if secret.ItemType == nil || secret.ItemName == nil { +// continue +// } + +// if *secret.ItemType == AKEYLESS_SECRET_TYPE_STATIC { +// staticSecrets = append(staticSecrets, *secret.ItemName) +// } else { +// nonStaticSecrets = append(nonStaticSecrets, secret) +// } +// } + +// return staticSecrets, nonStaticSecrets, nil +// } + +// processStaticSecrets processes all static secrets in a single batch request +// func processStaticSecrets(staticSecrets []string, akeylessSecretStore *akeylessSecretStore) (map[string]map[string]string, error) { +// if len(staticSecrets) == 0 { +// return make(map[string]map[string]string), nil +// } + +// // Get all static secrets in one batch request +// secretValue, err := GetSecretValueByType(staticSecrets, AKEYLESS_SECRET_TYPE_STATIC, akeylessSecretStore) +// if err != nil { +// return nil, fmt.Errorf("failed to get static secrets: %w", err) +// } + +// // Parse the response - static secrets return a map of secret names to values +// var secretsMap map[string]string +// if err := json.Unmarshal([]byte(secretValue.(string)), &secretsMap); err != nil { +// return nil, fmt.Errorf("failed to parse static secrets response: %w", err) +// } + +// // Convert to the expected format +// result := make(map[string]map[string]string) +// for secretName, secretValue := range secretsMap { +// result[secretName] = map[string]string{ +// secretName: secretValue, +// } +// } + +// return result, nil +// } + +// processNonStaticSecrets processes dynamic and rotated secrets asynchronously +// func processNonStaticSecrets(ctx context.Context, nonStaticSecrets []akeyless.Item, akeylessSecretStore *akeylessSecretStore) (map[string]map[string]string, error) { +// if len(nonStaticSecrets) == 0 { +// return make(map[string]map[string]string), nil +// } + +// // A WaitGroup to wait for all goroutines to finish +// var wg sync.WaitGroup + +// // A buffered channel to receive results from goroutines +// resultsChannel := make(chan result, len(nonStaticSecrets)) + +// // Launch a goroutine for each non-static secret +// for _, secret := range nonStaticSecrets { +// wg.Add(1) + +// currentSecret := secret + +// go func() { +// defer wg.Done() + +// // Fetch the secret value +// secretValue, err := GetSecretValueByTypeAsync(ctx, *currentSecret.ItemName, *currentSecret.ItemType, akeylessSecretStore) + +// // Parse the secret value based on its type +// parsedData, parseErr := parseSecretResponse(*currentSecret.ItemName, *currentSecret.ItemType, secretValue, err) + +// resultsChannel <- result{ +// key: *currentSecret.ItemName, +// value: parsedData, +// error: parseErr, +// } +// }() +// } + +// // Launch separate goroutine to close channel once all workers are done +// go func() { +// wg.Wait() +// close(resultsChannel) +// }() + +// // Collect results +// result := make(map[string]map[string]string) +// for res := range resultsChannel { +// if res.error != nil { +// return nil, fmt.Errorf("failed to get secret %s: %w", res.key, res.error) +// } +// result[res.key] = res.value.Data +// } + +// return result, nil +// } + +// parseSecretResponse parses the secret response based on the secret type +// func parseSecretResponse(secretName, secretType, secretValue any, err error) (*secretstores.GetSecretResponse, error) { +// if err != nil { +// return nil, err +// } + +// var secretData map[string]string + +// switch secretType { +// // TODO: Implement +// case AKEYLESS_SECRET_TYPE_DYNAMIC: +// var dynamicResp map[string]interface{} +// if err := json.Unmarshal([]byte(secretValue), &dynamicResp); err != nil { +// return nil, fmt.Errorf("failed to parse dynamic secret response: %w", err) +// } + +// case AKEYLESS_SECRET_TYPE_ROTATED: +// // TODO: Implement +// default: +// // For any other type, return as a simple key-value pair +// secretData = map[string]string{ +// secretName: secretValue, +// } +// } + +// return &secretstores.GetSecretResponse{ +// Data: secretData, +// }, nil +// } + +// GetSecretValueByTypeAsync gets the secret value by the type of the secret asynchronously +func GetSecretValueByTypeAsync(ctx context.Context, secretName any, secretType string, akeylessSecretStore *akeylessSecretStore) (any, error) { + return GetSecretValueByType(secretName, secretType, akeylessSecretStore) +} + +// transformStaticSecretResponse transforms the static secret response we get from Akeyless API +// into a map ready for Dapr. +// Static secrets can be of type text, json, email/password, key/value +// in case they are JSON/email/password, we need to transform the response into a string ready for Dapr. +func transformStaticSecretResponse(secrets map[string]any) (secretstores.GetSecretResponse, error) { + + secretData := make(map[string]string) + + for path, secret := range secrets { + switch secretType := secret.(type) { + case string: + secretData[path] = secretType + case map[string]any: + encoded, err := json.Marshal(secretType) + if err != nil { + return secretstores.GetSecretResponse{}, fmt.Errorf("failed to marshal secret response: %w", err) + } + secretData[path] = string(encoded) + } + + } + + return secretstores.GetSecretResponse{ + Data: secretData, + }, nil +} + +// type DynamicSecretResponse struct { +// ID string `json:"id"` +// Msg string `json:"msg"` +// Secret DynamicSecretSecret `json:"secret"` +// TTLInMinutes string `json:"ttl_in_minutes"` +// } + +// type DynamicSecretSecret struct { +// AppID string `json:"appId"` +// DisplayName string `json:"displayName"` +// EndDateTime string `json:"endDateTime"` +// KeyID string `json:"keyId"` +// SecretText string `json:"secretText"` +// TenantID string `json:"tenantId"` +// } + +// type DynamicSecretTransformedResponse struct { +// DynamicSecretName DynamicSecretCredentials `json:"secret_name"` +// } + +// type DynamicSecretCredentials struct { +// Username string `json:"username"` +// Password string `json:"password"` +// } + +// transformDynamicSecretResponse transforms the dynamic secret response we get from Akeyless API +// into a map ready for Dapr. +// func transformDynamicSecretResponse(dynamicSecretName string, dynamicResp DynamicSecretResponse) secretstores.GetSecretResponse { +// creds := DynamicSecretCredentials{ +// Username: dynamicResp.Secret.DisplayName, +// Password: dynamicResp.Secret.SecretText, +// } +// credsJSON, err := json.Marshal(creds) +// if err != nil { +// return secretstores.GetSecretResponse{ +// Data: map[string]string{}, +// } +// } +// return secretstores.GetSecretResponse{ +// Data: map[string]string{ +// dynamicSecretName: string(credsJSON), +// }, +// } +// } + +// type RotatedSecretResponse struct { +// Value RotatedSecretValue `json:"value"` +// } + +// type RotatedSecretValue struct { +// Username string `json:"username"` +// Password string `json:"password"` +// ApplicationID string `json:"application_id"` +// } + +// type RotatedSecretTransformedResponse struct { +// RotatedSecretName RotatedSecretCredentials `json:"secret_name"` +// } + +// type RotatedSecretCredentials struct { +// Username string `json:"username"` +// Password string `json:"password"` +// ApplicationID string `json:"application_id"` +// } + +// // transformDynamicSecretResponse transforms the dynamic secret response we get from Akeyless API +// // into a map ready for Dapr. +// func transformRotatedSecretResponse(rotatedSecretName string, rotatedResp RotatedSecretResponse) RotatedSecretTransformedResponse { +// return RotatedSecretTransformedResponse{ +// RotatedSecretName: RotatedSecretCredentials{ +// Username: rotatedResp.Value.Username, +// Password: rotatedResp.Value.Password, +// }, +// } +// } + +func GetDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { + return secretstores.GetSecretResponse{ + Data: map[string]string{ + secretName: secretValue, + }, + }, nil +} + +// func getDaprBulkSecretResponse(secrets map[string]map[string]string) secretstores.BulkGetSecretResponse { +// return secretstores.BulkGetSecretResponse{ +// Data: secrets, +// } +// } From 9ed774161d3c9f6e3766e68a513ed5cb8c2d8b9a Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 26 Sep 2025 18:02:09 -0400 Subject: [PATCH 07/28] added support for single static secret json/kv and password type Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless_test.go | 191 +++++++++++++++++++------ secretstores/akeyless/utils.go | 55 ++++--- 2 files changed, 188 insertions(+), 58 deletions(-) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index ec843a5992..a13aeeeadc 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -28,7 +28,6 @@ const ( // "iat": 1516239022 // } testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" - // testJSONStaticSecretName = "/path/to/json-static-secret" // testDynamicSecretName = "/path/to/dynamic-secret-test" // testRotatedSecretName = "/path/to/rotated-secret-test" testSecretValue = "r3vE4L3D" @@ -46,6 +45,19 @@ var ( ItemName: &mockDescribeStaticSecretName, ItemType: &mockDescribeStaticSecretType, } + mockStaticSecretJSONName = "/path/to/akeyless/static-secret-json-test" + mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ + mockStaticSecretJSONName: { + "some": "json", + }, + } + mockStaticSecretPasswordName = "/path/to/akeyless/static-secret-password-test" + mockGetSingleSecretPasswordValueResponse = map[string]map[string]string{ + mockStaticSecretPasswordName: { + "password": testSecretValue, + "username": "akeyless", + }, + } ) // var mockDescribeRotatedSecretItemResponse = akeyless.Item{ @@ -57,10 +69,6 @@ var mockGetSingleSecretValueResponse = map[string]string{ mockDescribeStaticSecretName: testSecretValue, } -// var mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ -// testJSONStaticSecretName: testJSONSecretValue, -// } - // var mockGetBulkSecretValueResponse = map[string]any{ // testStaticSecretName: testSecretValue + "1", // testStaticSecretName + "2": testSecretValue + "2", @@ -165,23 +173,23 @@ func TestMain(m *testing.M) { jsonResponse, _ := json.Marshal(mockGetSingleSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - // case "/get-dynamic-secret-value", "/v2/get-dynamic-secret-value": - // var dynamicResponse = DynamicSecretResponse{ - // ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", - // Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", - // Secret: DynamicSecretSecret{ - // AppID: "1234567890", - // DisplayName: "tmp.p-1234567890.GV7LR", - // EndDateTime: "2025-09-26T14:54:05.1643791Z", - // KeyID: "1234567890", - // SecretText: testSecretValue, - // TenantID: "1234567890", - // }, - // TTLInMinutes: "60", - // } - // jsonResponse, _ := json.Marshal(dynamicResponse) - // w.WriteHeader(http.StatusOK) - // w.Write(jsonResponse) + case "/get-dynamic-secret-value", "/v2/get-dynamic-secret-value": + var dynamicResponse = DynamicSecretResponse{ + ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", + Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", + Secret: DynamicSecretSecret{ + AppID: "1234567890", + DisplayName: "tmp.p-1234567890.GV7LR", + EndDateTime: "2025-09-26T14:54:05.1643791Z", + KeyID: "1234567890", + SecretText: testSecretValue, + TenantID: "1234567890", + }, + TTLInMinutes: "60", + } + jsonResponse, _ := json.Marshal(dynamicResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) // case "/get-rotated-secret-value", "/v2/get-rotated-secret-value": // var rotatedResponse = RotatedSecretResponse{ // Value: RotatedSecretValue{ @@ -215,9 +223,6 @@ func TestMain(m *testing.M) { // Run tests code := m.Run() - // Cleanup - mockGateway.Close() - // Exit with the same code as the tests os.Exit(code) } @@ -322,22 +327,6 @@ func TestInit(t *testing.T) { } } -func TestFeatures(t *testing.T) { - log := logger.NewLogger("test") - store := NewAkeylessSecretStore(log) - - features := store.Features() - assert.Empty(t, features) -} - -func TestClose(t *testing.T) { - log := logger.NewLogger("test") - store := NewAkeylessSecretStore(log) - - err := store.Close() - assert.NoError(t, err) -} - func TestParseMetadata(t *testing.T) { tests := []struct { name string @@ -553,6 +542,126 @@ func TestGetSecret(t *testing.T) { } } +func TestGetSingleSecretJSON(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleSecretJSONValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + mockDescribeItemResponse := akeyless.Item{ + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + } + jsonResponse, _ := json.Marshal(&mockDescribeItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + response, err := store.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: mockStaticSecretJSONName, + }) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretJSONName) + assert.Equal(t, "{\"some\":\"json\"}", response.Data[mockStaticSecretJSONName]) + + mockGateway.Close() +} + +func TestGetSingleSecretPassword(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth", "/v2/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single static secret value + case "/get-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleSecretPasswordValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + mockDescribeItemResponse := akeyless.Item{ + ItemName: &mockStaticSecretPasswordName, + ItemType: &mockDescribeStaticSecretType, + } + jsonResponse, _ := json.Marshal(&mockDescribeItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + response, err := store.GetSecret(context.Background(), secretstores.GetSecretRequest{ + Name: mockStaticSecretPasswordName, + }) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretPasswordName) + assert.Equal(t, "{\"password\":\"r3vE4L3D\",\"username\":\"akeyless\"}", response.Data[mockStaticSecretPasswordName]) + + mockGateway.Close() +} + // func TestBulkGetSecret(t *testing.T) { // // Setup a properly initialized store // store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 2d4b16d356..7d8e5d9dab 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -170,12 +170,33 @@ func GetSingleSecretValue(secretName string, secretType string, akeylessSecretSt return "", fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, err) } - typedSecretResp, ok := secretRespMap[secretName].(string) + // check if secret key is in response + value, ok := secretRespMap[secretName] if !ok { - return "", fmt.Errorf("failed to assert type of secret response to SingleStaticSecretResponse: %w", err) + return "", fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) + } + + // single static secrets can be of type string, or map[string]string + // if it's a map[string]string, we need to transform it to a string + switch valueType := value.(type) { + case string: + return valueType, nil + case map[string]string: + encoded, err := json.Marshal(valueType) + if err != nil { + return "", fmt.Errorf("failed to marshal secret response: %w", err) + } + return string(encoded), nil + case interface{}: + encoded, err := json.Marshal(valueType) + if err != nil { + return "", fmt.Errorf("failed to marshal secret response: %w", err) + } + return string(encoded), nil + default: + return "", fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) } - return typedSecretResp, nil // TODO implement dynamic secrets case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: return "", errors.New("dynamic secrets are not supported") @@ -490,21 +511,21 @@ func transformStaticSecretResponse(secrets map[string]any) (secretstores.GetSecr }, nil } -// type DynamicSecretResponse struct { -// ID string `json:"id"` -// Msg string `json:"msg"` -// Secret DynamicSecretSecret `json:"secret"` -// TTLInMinutes string `json:"ttl_in_minutes"` -// } +type DynamicSecretResponse struct { + ID string `json:"id"` + Msg string `json:"msg"` + Secret DynamicSecretSecret `json:"secret"` + TTLInMinutes string `json:"ttl_in_minutes"` +} -// type DynamicSecretSecret struct { -// AppID string `json:"appId"` -// DisplayName string `json:"displayName"` -// EndDateTime string `json:"endDateTime"` -// KeyID string `json:"keyId"` -// SecretText string `json:"secretText"` -// TenantID string `json:"tenantId"` -// } +type DynamicSecretSecret struct { + AppID string `json:"appId"` + DisplayName string `json:"displayName"` + EndDateTime string `json:"endDateTime"` + KeyID string `json:"keyId"` + SecretText string `json:"secretText"` + TenantID string `json:"tenantId"` +} // type DynamicSecretTransformedResponse struct { // DynamicSecretName DynamicSecretCredentials `json:"secret_name"` From 3307f2212a3edb95c2af4fee99aa60e42fc269dc Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 09:13:59 -0400 Subject: [PATCH 08/28] cleaned up getsinglesecretvalue to have one return statement Signed-off-by: Kobbi Gal --- secretstores/akeyless/utils.go | 40 ++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 7d8e5d9dab..81c350d0ba 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -161,40 +161,48 @@ func GetSecretType(secretName string, akeylessSecretStore *akeylessSecretStore) func GetSingleSecretValue(secretName string, secretType string, akeylessSecretStore *akeylessSecretStore) (string, error) { + var secretValue string + var err error + switch secretType { case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) getSecretValue.SetToken(akeylessSecretStore.token) - secretRespMap, _, err := akeylessSecretStore.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() - if err != nil { - return "", fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, err) + secretRespMap, _, apiErr := akeylessSecretStore.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) + break } // check if secret key is in response value, ok := secretRespMap[secretName] if !ok { - return "", fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) + break } // single static secrets can be of type string, or map[string]string // if it's a map[string]string, we need to transform it to a string switch valueType := value.(type) { case string: - return valueType, nil + secretValue = valueType case map[string]string: - encoded, err := json.Marshal(valueType) - if err != nil { - return "", fmt.Errorf("failed to marshal secret response: %w", err) + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response: %w", marshalErr) + } else { + secretValue = string(encoded) } - return string(encoded), nil - case interface{}: - encoded, err := json.Marshal(valueType) - if err != nil { - return "", fmt.Errorf("failed to marshal secret response: %w", err) + case any: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response: %w", marshalErr) + } else { + secretValue = string(encoded) } - return string(encoded), nil + default: - return "", fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) + err = fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) } // TODO implement dynamic secrets @@ -205,7 +213,7 @@ func GetSingleSecretValue(secretName string, secretType string, akeylessSecretSt return "", errors.New("rotated secrets are not supported") } - return "", nil + return secretValue, err } // GetSecretValueByType gets the secret value by the type of the secret. From d2ce2dd6ee8f467844217e6f8808e1e3c1fd9101 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 09:52:37 -0400 Subject: [PATCH 09/28] added single dynamic secret value support Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless_test.go | 186 +++++------ secretstores/akeyless/utils.go | 411 +++---------------------- 2 files changed, 113 insertions(+), 484 deletions(-) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index a13aeeeadc..353eb549aa 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -33,11 +33,6 @@ const ( testSecretValue = "r3vE4L3D" ) -// Mock responses for GetSecret and BulkGetSecret -// var testJSONSecretValue = map[string]string{ -// "some": "json", -// } - var ( mockDescribeStaticSecretName = "/path/to/akeyless/static-secret-test" mockDescribeStaticSecretType = AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE @@ -58,53 +53,31 @@ var ( "username": "akeyless", }, } + mockDescribeDynamicSecretName = "/path/to/akeyless/dynamic-secret-test" + mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE + mockDescribeDynamicSecretItemResponse = akeyless.Item{ + ItemName: &mockDescribeDynamicSecretName, + ItemType: &mockDescribeDynamicSecretType, + } + mockGetSingleDynamicSecretValueResponse = DynamicSecretResponse{ + ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", + Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", + Secret: DynamicSecretSecret{ + AppID: "1234567890", + DisplayName: "tmp.p-1234567890.GV7LR", + EndDateTime: "2025-09-26T14:54:05.1643791Z", + KeyID: "1234567890", + SecretText: testSecretValue, + TenantID: "1234567890", + }, + TTLInMinutes: "60", + } ) -// var mockDescribeRotatedSecretItemResponse = akeyless.Item{ -// "item_name": testRotatedSecretName, -// "item_type": AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE, -// } - var mockGetSingleSecretValueResponse = map[string]string{ mockDescribeStaticSecretName: testSecretValue, } -// var mockGetBulkSecretValueResponse = map[string]any{ -// testStaticSecretName: testSecretValue + "1", -// testStaticSecretName + "2": testSecretValue + "2", -// testJSONStaticSecretName: testJSONSecretValue, -// } - -// var mockListItemsResponse = map[string]interface{}{ -// "items": []map[string]string{ -// mockDescribeStaticSecretItemResponse, -// mockDescribeDynamicSecretItemResponse, -// mockDescribeRotatedSecretItemResponse, -// }, -// } - -// var mockGetDynamicSecretValueResponse = map[string]any{ -// "id": "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", -// "msg": "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", -// "secret": map[string]any{ -// "appId": "1234567890", -// "displayName": "tmp.p-1234567890.GV7LR", -// "endDateTime": "2025-09-26T14:54:05.1643791Z", -// "keyId": "1234567890", -// "secretText": testSecretValue, -// "tenantId": "1234567890", -// }, -// "ttl_in_minutes": "60", -// } - -// var mockGetRotatedSecretValueResponse = map[string]any{ -// "value": map[string]string{ -// "username": "abcdefghijklmnopqrstuvwxyz", -// "password": testSecretValue, -// "application_id": "1234567890", -// }, -// } - // Global mock server for all tests var mockGateway *httptest.Server @@ -160,7 +133,7 @@ func TestMain(m *testing.M) { // Handle different endpoints switch r.URL.Path { - case "/auth", "/v2/auth": + case "/auth": // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") @@ -169,11 +142,11 @@ func TestMain(m *testing.M) { w.WriteHeader(http.StatusOK) w.Write(jsonResponse) // Single static secret value - case "/get-secret-value", "/v2/get-secret-value": + case "/get-secret-value": jsonResponse, _ := json.Marshal(mockGetSingleSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - case "/get-dynamic-secret-value", "/v2/get-dynamic-secret-value": + case "/get-dynamic-secret-value": var dynamicResponse = DynamicSecretResponse{ ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", @@ -201,7 +174,7 @@ func TestMain(m *testing.M) { // jsonResponse, _ := json.Marshal(rotatedResponse) // w.WriteHeader(http.StatusOK) // w.Write(jsonResponse) - case "/list-items", "/v2/list-items": + case "/list-items": listItemsResponse := akeyless.NewListItemsInPathOutput() listItemsResponse.SetItems( []akeyless.Item{mockDescribeStaticSecretItemResponse}, @@ -209,7 +182,7 @@ func TestMain(m *testing.M) { jsonResponse, _ := json.Marshal(listItemsResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - case "/describe-item", "/v2/describe-item": + case "/describe-item": jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -507,14 +480,6 @@ func TestGetSecret(t *testing.T) { expectError: false, expectedSecret: testSecretValue, }, - { - name: "get existing test secret", - request: secretstores.GetSecretRequest{ - Name: mockDescribeStaticSecretName, - }, - expectError: false, - expectedSecret: testSecretValue, - }, // TODO: add non-existing secret test // { // name: "get non-existing secret", @@ -662,57 +627,6 @@ func TestGetSingleSecretPassword(t *testing.T) { mockGateway.Close() } -// func TestBulkGetSecret(t *testing.T) { -// // Setup a properly initialized store -// store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) -// meta := secretstores.Metadata{ -// Base: metadata.Base{ -// Properties: map[string]string{ -// "accessId": testAccessIdKey, -// "accessKey": testAccessKey, -// "gatewayUrl": mockGateway.URL, -// }, -// }, -// } - -// err := store.Init(context.Background(), meta) -// require.NoError(t, err) - -// // Test bulk get secret -// req := secretstores.BulkGetSecretRequest{} -// response, err := store.BulkGetSecret(context.Background(), req) - -// assert.NoError(t, err) -// assert.NotNil(t, response.Data) - -// // Check that we got the expected secrets -// expectedSecrets := []string{"my-secret", "test-secret", "json-secret"} -// for _, secretName := range expectedSecrets { -// assert.Contains(t, response.Data, secretName) -// } - -// // Check specific secret values -// assert.Equal(t, "{\"my-secret\":\"secret-value-123\"}", response.Data["my-secret"]["my-secret"]) -// assert.Equal(t, "{\"test-secret\":\"test-value-456\"}", response.Data["test-secret"]["test-secret"]) - -// // Check JSON secret (should be converted to string) -// jsonSecret := response.Data["json-secret"]["json-secret"] -// assert.Contains(t, jsonSecret, "username") -// assert.Contains(t, jsonSecret, "admin") -// assert.Contains(t, jsonSecret, "password") -// assert.Contains(t, jsonSecret, "secret123") -// } - -// func TestBulkGetSecretWithoutInit(t *testing.T) { -// // Test BulkGetSecret without initialization -// store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) - -// req := secretstores.BulkGetSecretRequest{} -// _, err := store.BulkGetSecret(context.Background(), req) -// assert.Error(t, err) -// assert.Contains(t, err.Error(), "not initialized") -// } - // Test GetSecretType functions func TestGetSecretType(t *testing.T) { // Test GetSecretType @@ -734,3 +648,55 @@ func TestGetSecretType(t *testing.T) { assert.NoError(t, err) assert.Equal(t, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE, secretType) } + +func TestGetSingleDynamicSecret(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-dynamic-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleDynamicSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(&mockDescribeDynamicSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + // Test GetSingleDynamicSecret + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + secretValue, err := GetSingleSecretValue(mockDescribeDynamicSecretName, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE, store) + assert.NoError(t, err) + assert.Equal(t, "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}", secretValue) + + mockGateway.Close() +} diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 81c350d0ba..6c5f2aada0 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -159,6 +159,8 @@ func GetSecretType(secretName string, akeylessSecretStore *akeylessSecretStore) return *describeItemResp.ItemType, nil } +// GetSingleSecretValue gets the value of a single secret from Akeyless. +// It returns the value of the secret or an error if the secret is not found. func GetSingleSecretValue(secretName string, secretType string, akeylessSecretStore *akeylessSecretStore) (string, error) { var secretValue string @@ -205,318 +207,45 @@ func GetSingleSecretValue(secretName string, secretType string, akeylessSecretSt err = fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) } - // TODO implement dynamic secrets case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: - return "", errors.New("dynamic secrets are not supported") - // TODO implement rotated secrets - case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: - return "", errors.New("rotated secrets are not supported") - } - - return secretValue, err -} - -// GetSecretValueByType gets the secret value by the type of the secret. -// It returns the secret value or an error if the secret value is not found. -// If secretName is not a string it means that we're getting numerous static secrets -// and we can get them all at once -func GetSecretValueByType(secretName any, secretType string, akeylessSecretStore *akeylessSecretStore) (secretstores.GetSecretResponse, error) { - var secretResp secretstores.GetSecretResponse - switch secretType { - case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: - var secrets []string - switch secretName := secretName.(type) { - case string: - secrets = []string{secretName} - case []string: - secrets = secretName - } - getSecretValue := akeyless.NewGetSecretValue(secrets) - getSecretValue.SetToken(akeylessSecretStore.token) - secretRespMap, _, err := akeylessSecretStore.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() - if err != nil { - return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName.(string), err) + getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) + getDynamicSecretValue.SetToken(akeylessSecretStore.token) + secretRespMap, _, apiErr := akeylessSecretStore.v2.GetDynamicSecretValue(context.Background()).Body(*getDynamicSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) + break } - transformedResp, err := transformStaticSecretResponse(secretRespMap) - if err != nil { - return secretstores.GetSecretResponse{}, fmt.Errorf("failed to transform static secret response for secret '%s': %w", secretName.(string), err) + // assert type of secretRespMap to DynamicSecretResponse + var dynamicSecretResp DynamicSecretResponse + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &dynamicSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response to DynamicSecretResponse: %w", unmarshalErr) + break } - secretResp = transformedResp - // case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: - // getSecretValue := akeyless.NewGetDynamicSecretValue(secretName.(string)) - // getSecretValue.SetToken(akeylessSecretStore.token) - // secretRespMap, _, err := akeylessSecretStore.v2.GetDynamicSecretValue(context.Background()).Body(*getSecretValue).Execute() - - // if err != nil { - // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName.(string), err) - // } - // // Convert secretRespMap (map[string]interface{}) to DynamicSecretResponse using a helper function - // dynamicSecretResp, err := transformDynamicSecretInterface(secretRespMap) - // if err != nil { - // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to transform dynamic secret response for secret '%s': %w", secretName.(string), err) - // } - - // transformedResp := transformDynamicSecretResponse(secretName.(string), dynamicSecretResp) - // secretResp = transformedResp - // case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: - // getSecretValue := akeyless.NewGetRotatedSecretValue(secretName.(string)) - // getSecretValue.SetToken(akeylessSecretStore.token) - // secretRespMap, _, err := akeylessSecretStore.v2.GetRotatedSecretValue(context.Background()).Body(*getSecretValue).Execute() - // if err != nil { - // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName.(string), err) - // } - - // // assert type of secretResp to RotatedSecretResponse - // rotatedSecretResp, ok := secretRespMap.(RotatedSecretResponse) - // if !ok { - // return secretstores.GetSecretResponse{}, fmt.Errorf("failed to assert type of secret response to RotatedSecretResponse: %w", err) - // } - - // // transform the response - // transformedResp := transformRotatedSecretResponse(secretName.(string), rotatedSecretResp) - // secretResp = transformedResp - default: - return secretstores.GetSecretResponse{}, errors.New("unsupported secret type") - } - return secretResp, nil -} - -// func transformDynamicSecretInterface(secretRespMap map[string]interface{}) (DynamicSecretResponse, error) { -// dynamicSecretResp := DynamicSecretResponse{} -// // Attempt to map fields from secretRespMap to dynamicSecretResp -// // This assumes DynamicSecretResponse is a struct and secretRespMap is a map[string]interface{} -// // Use mapstructure or manual assignment as needed -// err := mapstructure.Decode(secretRespMap, &dynamicSecretResp) -// if err != nil { -// return DynamicSecretResponse{}, fmt.Errorf("failed to decode secret response to DynamicSecretResponse: %w", err) -// } -// return dynamicSecretResp, nil -// } - -// convertSecretToString converts the secret to a string. -// It returns the secret value or an error if the secret value is not found. -// func convertSecretResponseToString(secret any) (string, error) { - -// // If the secret is a string, return it -// if secretStr, ok := secret.(string); ok { -// return secretStr, nil -// } - -// // If the secret is a map, marshal it to a JSON string -// secretValueBytes, err := json.Marshal(secret) -// if err != nil { -// return "", fmt.Errorf("failed to marshal secret response: %w", err) -// } - -// // Return the JSON string -// return string(secretValueBytes), nil -// } - -// result is a helper struct to pass results and errors back from goroutines -// type result struct { -// key string -// value *secretstores.GetSecretResponse -// error error -// } - -// BulkGetSecretResponse takes in a list of `akeyless.Item`, -// splits them into static and non-static subsets -// sends an async request to the appropriate API endpoint for each subset -// and returns them as the corresponding `secretstores.BulkGetSecretResponse` -// func BulkGetSecretResponse(ctx context.Context, allSecrets []akeyless.Item, akeylessSecretStore *akeylessSecretStore) (secretstores.BulkGetSecretResponse, error) { -// // Split secrets into static and non-static -// staticSecrets, nonStaticSecrets, err := splitSecretsByType(allSecrets) -// if err != nil { -// return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to split secrets by type: %w", err) -// } - -// akeylessSecretStore.logger.Debug("%d static secrets, %d non-static secrets", len(staticSecrets), len(nonStaticSecrets)) - -// // Process static secrets in batch -// staticResults, err := processStaticSecrets(staticSecrets, akeylessSecretStore) -// if err != nil { -// return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to process static secrets: %w", err) -// } - -// // Process non-static secrets asynchronously -// nonStaticResults, err := processNonStaticSecrets(ctx, nonStaticSecrets, akeylessSecretStore) -// if err != nil { -// return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to process non-static secrets: %w", err) -// } - -// // Combine all results -// allResults := make(map[string]map[string]string) -// maps.Copy(allResults, staticResults) -// maps.Copy(allResults, nonStaticResults) - -// return secretstores.BulkGetSecretResponse{ -// Data: allResults, -// }, nil -// } - -// splitSecretsByType splits secrets into static and non-static (dynamic, rotated) secrets -// func splitSecretsByType(allSecrets []akeyless.Item) ([]string, []akeyless.Item, error) { -// var staticSecrets []string -// var nonStaticSecrets []akeyless.Item - -// for _, secret := range allSecrets { -// if secret.ItemType == nil || secret.ItemName == nil { -// continue -// } - -// if *secret.ItemType == AKEYLESS_SECRET_TYPE_STATIC { -// staticSecrets = append(staticSecrets, *secret.ItemName) -// } else { -// nonStaticSecrets = append(nonStaticSecrets, secret) -// } -// } - -// return staticSecrets, nonStaticSecrets, nil -// } - -// processStaticSecrets processes all static secrets in a single batch request -// func processStaticSecrets(staticSecrets []string, akeylessSecretStore *akeylessSecretStore) (map[string]map[string]string, error) { -// if len(staticSecrets) == 0 { -// return make(map[string]map[string]string), nil -// } - -// // Get all static secrets in one batch request -// secretValue, err := GetSecretValueByType(staticSecrets, AKEYLESS_SECRET_TYPE_STATIC, akeylessSecretStore) -// if err != nil { -// return nil, fmt.Errorf("failed to get static secrets: %w", err) -// } - -// // Parse the response - static secrets return a map of secret names to values -// var secretsMap map[string]string -// if err := json.Unmarshal([]byte(secretValue.(string)), &secretsMap); err != nil { -// return nil, fmt.Errorf("failed to parse static secrets response: %w", err) -// } - -// // Convert to the expected format -// result := make(map[string]map[string]string) -// for secretName, secretValue := range secretsMap { -// result[secretName] = map[string]string{ -// secretName: secretValue, -// } -// } - -// return result, nil -// } - -// processNonStaticSecrets processes dynamic and rotated secrets asynchronously -// func processNonStaticSecrets(ctx context.Context, nonStaticSecrets []akeyless.Item, akeylessSecretStore *akeylessSecretStore) (map[string]map[string]string, error) { -// if len(nonStaticSecrets) == 0 { -// return make(map[string]map[string]string), nil -// } - -// // A WaitGroup to wait for all goroutines to finish -// var wg sync.WaitGroup - -// // A buffered channel to receive results from goroutines -// resultsChannel := make(chan result, len(nonStaticSecrets)) - -// // Launch a goroutine for each non-static secret -// for _, secret := range nonStaticSecrets { -// wg.Add(1) - -// currentSecret := secret - -// go func() { -// defer wg.Done() - -// // Fetch the secret value -// secretValue, err := GetSecretValueByTypeAsync(ctx, *currentSecret.ItemName, *currentSecret.ItemType, akeylessSecretStore) - -// // Parse the secret value based on its type -// parsedData, parseErr := parseSecretResponse(*currentSecret.ItemName, *currentSecret.ItemType, secretValue, err) - -// resultsChannel <- result{ -// key: *currentSecret.ItemName, -// value: parsedData, -// error: parseErr, -// } -// }() -// } - -// // Launch separate goroutine to close channel once all workers are done -// go func() { -// wg.Wait() -// close(resultsChannel) -// }() - -// // Collect results -// result := make(map[string]map[string]string) -// for res := range resultsChannel { -// if res.error != nil { -// return nil, fmt.Errorf("failed to get secret %s: %w", res.key, res.error) -// } -// result[res.key] = res.value.Data -// } - -// return result, nil -// } - -// parseSecretResponse parses the secret response based on the secret type -// func parseSecretResponse(secretName, secretType, secretValue any, err error) (*secretstores.GetSecretResponse, error) { -// if err != nil { -// return nil, err -// } - -// var secretData map[string]string - -// switch secretType { -// // TODO: Implement -// case AKEYLESS_SECRET_TYPE_DYNAMIC: -// var dynamicResp map[string]interface{} -// if err := json.Unmarshal([]byte(secretValue), &dynamicResp); err != nil { -// return nil, fmt.Errorf("failed to parse dynamic secret response: %w", err) -// } - -// case AKEYLESS_SECRET_TYPE_ROTATED: -// // TODO: Implement -// default: -// // For any other type, return as a simple key-value pair -// secretData = map[string]string{ -// secretName: secretValue, -// } -// } - -// return &secretstores.GetSecretResponse{ -// Data: secretData, -// }, nil -// } - -// GetSecretValueByTypeAsync gets the secret value by the type of the secret asynchronously -func GetSecretValueByTypeAsync(ctx context.Context, secretName any, secretType string, akeylessSecretStore *akeylessSecretStore) (any, error) { - return GetSecretValueByType(secretName, secretType, akeylessSecretStore) -} - -// transformStaticSecretResponse transforms the static secret response we get from Akeyless API -// into a map ready for Dapr. -// Static secrets can be of type text, json, email/password, key/value -// in case they are JSON/email/password, we need to transform the response into a string ready for Dapr. -func transformStaticSecretResponse(secrets map[string]any) (secretstores.GetSecretResponse, error) { - - secretData := make(map[string]string) - for path, secret := range secrets { - switch secretType := secret.(type) { - case string: - secretData[path] = secretType - case map[string]any: - encoded, err := json.Marshal(secretType) - if err != nil { - return secretstores.GetSecretResponse{}, fmt.Errorf("failed to marshal secret response: %w", err) - } - secretData[path] = string(encoded) + // take only relevant fields (DisplayName and SecretText) from response and marshal it to a JSON string + dynamicSecretResp.Secret.AppID = "" + dynamicSecretResp.Secret.EndDateTime = "" + dynamicSecretResp.Secret.KeyID = "" + dynamicSecretResp.Secret.TenantID = "" + jsonBytes, marshalErr = json.Marshal(dynamicSecretResp.Secret) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break } + secretValue = string(jsonBytes) + // TODO implement rotated secrets + case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + return "", errors.New("rotated secrets are not supported") } - return secretstores.GetSecretResponse{ - Data: secretData, - }, nil + return secretValue, err } type DynamicSecretResponse struct { @@ -527,74 +256,14 @@ type DynamicSecretResponse struct { } type DynamicSecretSecret struct { - AppID string `json:"appId"` + AppID string `json:"appId,omitempty"` DisplayName string `json:"displayName"` - EndDateTime string `json:"endDateTime"` - KeyID string `json:"keyId"` - SecretText string `json:"secretText"` - TenantID string `json:"tenantId"` + EndDateTime string `json:"endDateTime,omitempty"` + KeyID string `json:"keyId,omitempty"` + SecretText string `json:"secretText,omitempty"` + TenantID string `json:"tenantId,omitempty"` } -// type DynamicSecretTransformedResponse struct { -// DynamicSecretName DynamicSecretCredentials `json:"secret_name"` -// } - -// type DynamicSecretCredentials struct { -// Username string `json:"username"` -// Password string `json:"password"` -// } - -// transformDynamicSecretResponse transforms the dynamic secret response we get from Akeyless API -// into a map ready for Dapr. -// func transformDynamicSecretResponse(dynamicSecretName string, dynamicResp DynamicSecretResponse) secretstores.GetSecretResponse { -// creds := DynamicSecretCredentials{ -// Username: dynamicResp.Secret.DisplayName, -// Password: dynamicResp.Secret.SecretText, -// } -// credsJSON, err := json.Marshal(creds) -// if err != nil { -// return secretstores.GetSecretResponse{ -// Data: map[string]string{}, -// } -// } -// return secretstores.GetSecretResponse{ -// Data: map[string]string{ -// dynamicSecretName: string(credsJSON), -// }, -// } -// } - -// type RotatedSecretResponse struct { -// Value RotatedSecretValue `json:"value"` -// } - -// type RotatedSecretValue struct { -// Username string `json:"username"` -// Password string `json:"password"` -// ApplicationID string `json:"application_id"` -// } - -// type RotatedSecretTransformedResponse struct { -// RotatedSecretName RotatedSecretCredentials `json:"secret_name"` -// } - -// type RotatedSecretCredentials struct { -// Username string `json:"username"` -// Password string `json:"password"` -// ApplicationID string `json:"application_id"` -// } - -// // transformDynamicSecretResponse transforms the dynamic secret response we get from Akeyless API -// // into a map ready for Dapr. -// func transformRotatedSecretResponse(rotatedSecretName string, rotatedResp RotatedSecretResponse) RotatedSecretTransformedResponse { -// return RotatedSecretTransformedResponse{ -// RotatedSecretName: RotatedSecretCredentials{ -// Username: rotatedResp.Value.Username, -// Password: rotatedResp.Value.Password, -// }, -// } -// } - func GetDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { return secretstores.GetSecretResponse{ Data: map[string]string{ @@ -602,9 +271,3 @@ func GetDaprSingleSecretResponse(secretName string, secretValue string) (secrets }, }, nil } - -// func getDaprBulkSecretResponse(secrets map[string]map[string]string) secretstores.BulkGetSecretResponse { -// return secretstores.BulkGetSecretResponse{ -// Data: secrets, -// } -// } From 9c253e5a18d4bd69737a6e931a8114fbb61a5a88 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 10:00:27 -0400 Subject: [PATCH 10/28] added single rotated secret value support Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless_test.go | 93 +++++++++++++++++++------- secretstores/akeyless/utils.go | 40 ++++++++++- 2 files changed, 105 insertions(+), 28 deletions(-) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 353eb549aa..6dea32365c 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -72,6 +72,19 @@ var ( }, TTLInMinutes: "60", } + mockDescribeRotatedSecretName = "/path/to/akeyless/rotated-secret-test" + mockDescribeRotatedSecretType = AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE + mockDescribeRotatedSecretItemResponse = akeyless.Item{ + ItemName: &mockDescribeRotatedSecretName, + ItemType: &mockDescribeRotatedSecretType, + } + mockGetSingleRotatedSecretValueResponse = RotatedSecretResponse{ + Value: RotatedSecretValue{ + Username: "abcdefghijklmnopqrstuvwxyz", + Password: testSecretValue, + ApplicationID: "1234567890", + }, + } ) var mockGetSingleSecretValueResponse = map[string]string{ @@ -146,34 +159,10 @@ func TestMain(m *testing.M) { jsonResponse, _ := json.Marshal(mockGetSingleSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - case "/get-dynamic-secret-value": - var dynamicResponse = DynamicSecretResponse{ - ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", - Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", - Secret: DynamicSecretSecret{ - AppID: "1234567890", - DisplayName: "tmp.p-1234567890.GV7LR", - EndDateTime: "2025-09-26T14:54:05.1643791Z", - KeyID: "1234567890", - SecretText: testSecretValue, - TenantID: "1234567890", - }, - TTLInMinutes: "60", - } - jsonResponse, _ := json.Marshal(dynamicResponse) + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - // case "/get-rotated-secret-value", "/v2/get-rotated-secret-value": - // var rotatedResponse = RotatedSecretResponse{ - // Value: RotatedSecretValue{ - // Username: "abcdefghijklmnopqrstuvwxyz", - // Password: testSecretValue, - // ApplicationID: "1234567890", - // }, - // } - // jsonResponse, _ := json.Marshal(rotatedResponse) - // w.WriteHeader(http.StatusOK) - // w.Write(jsonResponse) case "/list-items": listItemsResponse := akeyless.NewListItemsInPathOutput() listItemsResponse.SetItems( @@ -700,3 +689,55 @@ func TestGetSingleDynamicSecret(t *testing.T) { mockGateway.Close() } + +func TestGetSingleRotatedSecret(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(&mockDescribeRotatedSecretItemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + // Test GetSingleRotatedSecret + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + secretValue, err := GetSingleSecretValue(mockDescribeRotatedSecretName, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE, store) + assert.NoError(t, err) + assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", secretValue) + + mockGateway.Close() +} diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 6c5f2aada0..b62f75e258 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -240,9 +240,35 @@ func GetSingleSecretValue(secretName string, secretType string, akeylessSecretSt } secretValue = string(jsonBytes) - // TODO implement rotated secrets case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: - return "", errors.New("rotated secrets are not supported") + getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) + getRotatedSecretValue.SetToken(akeylessSecretStore.token) + secretRespMap, _, apiErr := akeylessSecretStore.v2.GetRotatedSecretValue(context.Background()).Body(*getRotatedSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + // assert type of secretRespMap to RotatedSecretResponse + var rotatedSecretResp RotatedSecretResponse + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &rotatedSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response to RotatedSecretResponse: %w", unmarshalErr) + break + } + + // take only relevant fields (Username and Password) from response and marshal it to a JSON string + rotatedSecretResp.Value.ApplicationID = "" + jsonBytes, marshalErr = json.Marshal(rotatedSecretResp.Value) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + secretValue = string(jsonBytes) } return secretValue, err @@ -264,6 +290,16 @@ type DynamicSecretSecret struct { TenantID string `json:"tenantId,omitempty"` } +type RotatedSecretResponse struct { + Value RotatedSecretValue `json:"value"` +} + +type RotatedSecretValue struct { + Username string `json:"username"` + Password string `json:"password"` + ApplicationID string `json:"application_id,omitempty"` +} + func GetDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { return secretstores.GetSecretResponse{ Data: map[string]string{ From 1145b6055b0e2625f63f53e3f1525313b573438e Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 14:12:05 -0400 Subject: [PATCH 11/28] moved funcs to ak store receiver wip: get bulk secrets Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 381 ++++++++++++++++++++++++- secretstores/akeyless/akeyless_test.go | 7 +- secretstores/akeyless/utils.go | 244 ++++------------ 3 files changed, 424 insertions(+), 208 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 73caf3f8ee..d13db7d660 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -2,10 +2,13 @@ package akeyless import ( "context" + "encoding/json" "errors" "fmt" "reflect" + "sync" + aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" "github.com/akeylesslabs/akeyless-go/v5" "github.com/dapr/components-contrib/metadata" @@ -49,7 +52,7 @@ func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metada return errors.New("failed to parse metadata: " + err.Error()) } - err = Authenticate(m, a) + err = a.Authenticate(m) if err != nil { return errors.New("failed to authenticate with Akeyless: " + err.Error()) } @@ -64,14 +67,14 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge } a.logger.Debug("getting secret type for '%s'...", req.Name) - secretType, err := GetSecretType(req.Name, a) + secretType, err := a.GetSecretType(req.Name) if err != nil { return secretstores.GetSecretResponse{}, err } a.logger.Debug("getting secret value for '%s' (type %s)...", req.Name, secretType) - secretValue, err := GetSingleSecretValue(req.Name, secretType, a) + secretValue, err := a.GetSingleSecretValue(req.Name, secretType) if err != nil { return secretstores.GetSecretResponse{}, errors.New(err.Error()) } @@ -82,27 +85,130 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge } // BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. +// The method performs the following steps: +// 1. Recursively list all items in Akeyless +// 2. Separate items by type since only static secrets are supported for bulk get +// 3. Get secret values concurrently, each item type in a separate goroutine func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) { if a.v2 == nil { return secretstores.BulkGetSecretResponse{}, errors.New("akeyless client not initialized") } - // For bulk get, we need to list all secrets first - listItems := akeyless.NewListItems() - listItems.SetToken(a.token) - listItems.SetPath("/") - listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) + // initialize response + response := secretstores.BulkGetSecretResponse{ + Data: make(map[string]map[string]string), + } - // Execute the list items request - itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() + // For bulk get, we need to list all secrets first + a.logger.Debug("listing items from / path...") + listItems, err := a.listItemsRecursively("/") if err != nil { - return secretstores.BulkGetSecretResponse{}, fmt.Errorf("failed to list items from Akeyless: %w", err) + return response, fmt.Errorf("failed to list items from Akeyless: %w", err) + } + + // if no items returned, return empty response + if len(listItems) == 0 { + a.logger.Debug("no items returned from / path") + return response, nil + } + + // separate items by type since only static secrets are supported for bulk get + staticItems, dynamicItems, rotatedItems := a.separateItemsByType(listItems) + a.logger.Info("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItems), len(dynamicItems), len(rotatedItems)) + + // listItems can get quite large, so we don't need all item details, we can use the item names instead + // and free memory + listItems = nil + staticItemNames := GetItemNames(staticItems) + dynamicItemNames := GetItemNames(dynamicItems) + rotatedItemNames := GetItemNames(rotatedItems) + a.logger.Debug("static items: %v", staticItemNames) + a.logger.Debug("dynamic items: %v", dynamicItemNames) + a.logger.Debug("rotated items: %v", rotatedItemNames) + + haveStaticItems := len(staticItemNames) > 0 + haveDynamicItems := len(dynamicItemNames) > 0 + haveRotatedItems := len(rotatedItemNames) > 0 + + secretResultChannels := make(chan secretResultCollection, boolToInt(haveStaticItems)+boolToInt(haveDynamicItems)+boolToInt(haveRotatedItems)) + + mutex := sync.Mutex{} + + // get secret values concurrently, each item type in a separate goroutine + wg := sync.WaitGroup{} + if haveStaticItems { + wg.Add(1) + go func() { + defer wg.Done() + if len(staticItemNames) == 1 { + staticSecretName := staticItemNames[0] + value, err := a.GetSingleSecretValue(staticSecretName, AKEYLESS_SECRET_TYPE_STATIC) + if err != nil { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} + } else { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: nil} + } + } else { + secretResponse := a.GetBulkStaticSecretValues(staticItemNames) + if len(secretResponse) > 0 { + for _, result := range secretResponse { + secretResultChannels <- result + } + } + } + }() + } + if haveDynamicItems { + wg.Add(1) + go func() { + defer wg.Done() + for _, item := range dynamicItemNames { + value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_DYNAMIC) + if err != nil { + secretResultChannels <- secretResultCollection{name: item, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: item, value: value, err: nil} + } + } + }() + } + if haveRotatedItems { + wg.Add(1) + go func() { + defer wg.Done() + for _, item := range rotatedItemNames { + value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_ROTATED) + if err != nil { + secretResultChannels <- secretResultCollection{name: item, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: item, value: value, err: nil} + } + } + }() + } + + // close the channel when all goroutines are done + go func() { + wg.Wait() + close(secretResultChannels) + }() + + // collect results and populate response + for result := range secretResultChannels { + if result.err != nil { + a.logger.Error("error getting secret '%s': %s. Skipping...", result.name, result.err.Error()) + continue + } + + // lock the mutex to prevent race conditions + mutex.Lock() + response.Data[result.name] = map[string]string{result.name: result.value} + mutex.Unlock() } - a.logger.Debug("%d items returned from Akeyless", len(itemsList.Items)) // Use the new BulkGetSecretResponse function to handle all secret types properly // return BulkGetSecretResponse(ctx, itemsList.Items, a) - return secretstores.BulkGetSecretResponse{}, nil + return response, nil } // Features returns the features available in this secret store. @@ -175,3 +281,252 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle return &m, nil } + +func (a *akeylessSecretStore) GetSecretType(secretName string) (string, error) { + describeItem := akeyless.NewDescribeItem(secretName) + describeItem.SetToken(a.token) + describeItemResp, _, err := a.v2.DescribeItem(context.Background()).Body(*describeItem).Execute() + if err != nil { + return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) + } + + if describeItemResp.ItemType == nil { + return "", errors.New("unable to retrieve secret type, missing type in describe item response") + } + + return *describeItemResp.ItemType, nil +} + +// GetSingleSecretValue gets the value of a single secret from Akeyless. +// It returns the value of the secret or an error if the secret is not found. +func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType string) (string, error) { + + var secretValue string + var err error + + switch secretType { + case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) + getSecretValue.SetToken(a.token) + secretRespMap, _, apiErr := a.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) + break + } + + // check if secret key is in response + value, ok := secretRespMap[secretName] + if !ok { + err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) + break + } + + // single static secrets can be of type string, or map[string]string + // if it's a map[string]string, we need to transform it to a string + secretValue, err = stringifyStaticSecret(value, secretName) + if err != nil { + err = fmt.Errorf("failed to stringify static secret '%s': %w", secretName, err) + break + } + + case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) + getDynamicSecretValue.SetToken(a.token) + secretRespMap, _, apiErr := a.v2.GetDynamicSecretValue(context.Background()).Body(*getDynamicSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + // assert type of secretRespMap to DynamicSecretResponse + var dynamicSecretResp DynamicSecretResponse + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &dynamicSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response to DynamicSecretResponse: %w", unmarshalErr) + break + } + + // take only relevant fields (DisplayName and SecretText) from response and marshal it to a JSON string + dynamicSecretResp.Secret.AppID = "" + dynamicSecretResp.Secret.EndDateTime = "" + dynamicSecretResp.Secret.KeyID = "" + dynamicSecretResp.Secret.TenantID = "" + jsonBytes, marshalErr = json.Marshal(dynamicSecretResp.Secret) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + secretValue = string(jsonBytes) + + case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) + getRotatedSecretValue.SetToken(a.token) + secretRespMap, _, apiErr := a.v2.GetRotatedSecretValue(context.Background()).Body(*getRotatedSecretValue).Execute() + if apiErr != nil { + err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) + break + } + + // assert type of secretRespMap to RotatedSecretResponse + var rotatedSecretResp RotatedSecretResponse + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &rotatedSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response to RotatedSecretResponse: %w", unmarshalErr) + break + } + + // take only relevant fields (Username and Password) from response and marshal it to a JSON string + rotatedSecretResp.Value.ApplicationID = "" + jsonBytes, marshalErr = json.Marshal(rotatedSecretResp.Value) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + break + } + secretValue = string(jsonBytes) + } + + return secretValue, err +} + +// GetBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. +// It returns a map of secret names and their values. +func (a *akeylessSecretStore) GetBulkStaticSecretValues(secretNames []string) []secretResultCollection { + + var secretResponse = make([]secretResultCollection, len(secretNames)) + + getSecretsValues := akeyless.NewGetSecretValue(secretNames) + getSecretsValues.SetToken(a.token) + secretRespMap, _, apiErr := a.v2.GetSecretValue(context.Background()).Body(*getSecretsValues).Execute() + if apiErr != nil { + secretResponse = append(secretResponse, secretResultCollection{name: "", value: "", err: fmt.Errorf("failed to get static secrets' '%s' value from Akeyless API: %w", secretNames, apiErr)}) + } else { + for secretName, secretValue := range secretRespMap { + value, err := stringifyStaticSecret(secretValue, secretName) + secretResponse = append(secretResponse, secretResultCollection{name: secretName, value: value, err: err}) + } + } + + return secretResponse +} + +// listItemsRecursively lists all items in a given path recursively. +// It returns a list of items and an error if the list items request fails. +func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item, error) { + var allItems []akeyless.Item + + // Create the list items request + listItems := akeyless.NewListItems() + listItems.SetToken(a.token) + listItems.SetPath(path) + listItems.SetMinimalView(true) + listItems.SetAutoPagination("enabled") + listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) + + // Execute the list items request + itemsList, _, err := a.v2.ListItems(context.Background()).Body(*listItems).Execute() + if err != nil { + return nil, err + } + + // Add items from current path + if itemsList.Items != nil { + allItems = append(allItems, itemsList.Items...) + } + + // Recursively process each subfolder + if itemsList.Folders != nil { + for _, folder := range itemsList.Folders { + subItems, err := a.listItemsRecursively(folder) + if err != nil { + return nil, err + } + allItems = append(allItems, subItems...) + } + } + + return allItems, nil +} + +// Authenticate authenticates with Akeyless using the provided metadata. +// It returns an error if the authentication fails. +func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { + + a.logger.Debug("Creating authentication request to Akeyless...") + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + authRequest.SetAccessType(metadata.AccessType) + + // Depending on the access type we set the appropriate authentication method + switch metadata.AccessType { + // If access type is AWS IAM we use the cloud ID + case AKEYLESS_AUTH_ACCESS_IAM: + a.logger.Debug("getting cloud ID for AWS IAM...") + id, err := aws.GetCloudId() + if err != nil { + return errors.New("unable to get cloud ID") + } + authRequest.SetCloudId(id) + case AKEYLESS_AUTH_ACCESS_JWT: + a.logger.Debug("setting JWT for authentication...") + authRequest.SetJwt(metadata.JWT) + case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + a.logger.Debug("setting access key for authentication...") + authRequest.SetAccessKey(metadata.AccessKey) + } + + // Create Akeyless API client configuration + a.logger.Debug("creating Akeyless API client configuration...") + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = AKEYLESS_USER_AGENT + config.AddDefaultHeader("akeylessclienttype", AKEYLESS_USER_AGENT) + + a.v2 = akeyless.NewAPIClient(config).V2Api + + a.logger.Debug("authenticating with Akeyless...") + out, _, err := a.v2.Auth(context.Background()).Body(*authRequest).Execute() + if err != nil { + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + + a.logger.Debug("setting token %s for authentication...", out.GetToken()[:3]+"[REDACTED]") + a.logger.Debug("expires at: %s", out.GetExpiration()) + a.token = out.GetToken() + + return nil +} + +func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akeyless.Item, []akeyless.Item, []akeyless.Item) { + staticItems := []akeyless.Item{} + dynamicItems := []akeyless.Item{} + rotatedItems := []akeyless.Item{} + for _, item := range items { + itemType, err := a.GetSecretType(*item.ItemName) + if err != nil { + continue + } + + if itemType == AKEYLESS_SECRET_TYPE_STATIC { + staticItems = append(staticItems, item) + } + if itemType == AKEYLESS_SECRET_TYPE_DYNAMIC { + dynamicItems = append(dynamicItems, item) + } + if itemType == AKEYLESS_SECRET_TYPE_ROTATED { + rotatedItems = append(rotatedItems, item) + } + } + return staticItems, dynamicItems, rotatedItems +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 6dea32365c..b606dbe8d1 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -469,7 +469,6 @@ func TestGetSecret(t *testing.T) { expectError: false, expectedSecret: testSecretValue, }, - // TODO: add non-existing secret test // { // name: "get non-existing secret", // request: secretstores.GetSecretRequest{ @@ -633,7 +632,7 @@ func TestGetSecretType(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) - secretType, err := GetSecretType(mockDescribeStaticSecretName, store) + secretType, err := store.GetSecretType(mockDescribeStaticSecretName) assert.NoError(t, err) assert.Equal(t, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE, secretType) } @@ -683,7 +682,7 @@ func TestGetSingleDynamicSecret(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) - secretValue, err := GetSingleSecretValue(mockDescribeDynamicSecretName, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE, store) + secretValue, err := store.GetSingleSecretValue(mockDescribeDynamicSecretName, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE) assert.NoError(t, err) assert.Equal(t, "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}", secretValue) @@ -735,7 +734,7 @@ func TestGetSingleRotatedSecret(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) - secretValue, err := GetSingleSecretValue(mockDescribeRotatedSecretName, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE, store) + secretValue, err := store.GetSingleSecretValue(mockDescribeRotatedSecretName, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE) assert.NoError(t, err) assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", secretValue) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index b62f75e258..5acdb380d8 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -1,15 +1,12 @@ package akeyless import ( - "context" + "encoding/json" "errors" "fmt" "regexp" "strings" - "encoding/json" - - aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" "github.com/akeylesslabs/akeyless-go/v5" "github.com/dapr/components-contrib/secretstores" ) @@ -88,192 +85,6 @@ func GetAccessTypeDisplayName(typeChar string) (string, error) { return displayName, nil } -// Authenticate authenticates with Akeyless using the provided metadata. -// It returns an error if the authentication fails. -func Authenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { - - akeylessSecretStore.logger.Debug("Creating authentication request to Akeyless...") - authRequest := akeyless.NewAuth() - authRequest.SetAccessId(metadata.AccessID) - authRequest.SetAccessType(metadata.AccessType) - - // Depending on the access type we set the appropriate authentication method - switch metadata.AccessType { - // If access type is AWS IAM we use the cloud ID - case AKEYLESS_AUTH_ACCESS_IAM: - akeylessSecretStore.logger.Debug("getting cloud ID for AWS IAM...") - id, err := aws.GetCloudId() - if err != nil { - return errors.New("unable to get cloud ID") - } - authRequest.SetCloudId(id) - case AKEYLESS_AUTH_ACCESS_JWT: - akeylessSecretStore.logger.Debug("setting JWT for authentication...") - authRequest.SetJwt(metadata.JWT) - case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: - akeylessSecretStore.logger.Debug("setting access key for authentication...") - authRequest.SetAccessKey(metadata.AccessKey) - } - - // Create Akeyless API client configuration - akeylessSecretStore.logger.Debug("creating Akeyless API client configuration...") - config := akeyless.NewConfiguration() - config.Servers = []akeyless.ServerConfiguration{ - { - URL: metadata.GatewayURL, - }, - } - config.UserAgent = AKEYLESS_USER_AGENT - config.AddDefaultHeader("akeylessclienttype", AKEYLESS_USER_AGENT) - - akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api - - akeylessSecretStore.logger.Debug("authenticating with Akeyless...") - out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() - if err != nil { - return fmt.Errorf("failed to authenticate with Akeyless: %w", err) - } - - akeylessSecretStore.logger.Debug("setting token %s for authentication...", out.GetToken()[:3]+"[REDACTED]") - akeylessSecretStore.logger.Debug("expires at: %s", out.GetExpiration()) - akeylessSecretStore.token = out.GetToken() - - return nil -} - -// getSecretType gets the type of the secret from the describe item response. -// It returns the type of the secret (e.g. static, dynamic, rotated) or an error if the type is unknown. -func GetSecretType(secretName string, akeylessSecretStore *akeylessSecretStore) (string, error) { - - describeItem := akeyless.NewDescribeItem(secretName) - describeItem.SetToken(akeylessSecretStore.token) - describeItemResp, _, err := akeylessSecretStore.v2.DescribeItem(context.Background()).Body(*describeItem).Execute() - if err != nil { - return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) - } - - if describeItemResp.ItemType == nil { - return "", errors.New("unable to retrieve secret type, missing type in describe item response") - } - - return *describeItemResp.ItemType, nil -} - -// GetSingleSecretValue gets the value of a single secret from Akeyless. -// It returns the value of the secret or an error if the secret is not found. -func GetSingleSecretValue(secretName string, secretType string, akeylessSecretStore *akeylessSecretStore) (string, error) { - - var secretValue string - var err error - - switch secretType { - case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: - getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) - getSecretValue.SetToken(akeylessSecretStore.token) - secretRespMap, _, apiErr := akeylessSecretStore.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() - if apiErr != nil { - err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) - break - } - - // check if secret key is in response - value, ok := secretRespMap[secretName] - if !ok { - err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: key not found", secretName) - break - } - - // single static secrets can be of type string, or map[string]string - // if it's a map[string]string, we need to transform it to a string - switch valueType := value.(type) { - case string: - secretValue = valueType - case map[string]string: - encoded, marshalErr := json.Marshal(valueType) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response: %w", marshalErr) - } else { - secretValue = string(encoded) - } - case any: - encoded, marshalErr := json.Marshal(valueType) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response: %w", marshalErr) - } else { - secretValue = string(encoded) - } - - default: - err = fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) - } - - case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: - getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) - getDynamicSecretValue.SetToken(akeylessSecretStore.token) - secretRespMap, _, apiErr := akeylessSecretStore.v2.GetDynamicSecretValue(context.Background()).Body(*getDynamicSecretValue).Execute() - if apiErr != nil { - err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) - break - } - - // assert type of secretRespMap to DynamicSecretResponse - var dynamicSecretResp DynamicSecretResponse - jsonBytes, marshalErr := json.Marshal(secretRespMap) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) - break - } - if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &dynamicSecretResp); unmarshalErr != nil { - err = fmt.Errorf("failed to unmarshal secret response to DynamicSecretResponse: %w", unmarshalErr) - break - } - - // take only relevant fields (DisplayName and SecretText) from response and marshal it to a JSON string - dynamicSecretResp.Secret.AppID = "" - dynamicSecretResp.Secret.EndDateTime = "" - dynamicSecretResp.Secret.KeyID = "" - dynamicSecretResp.Secret.TenantID = "" - jsonBytes, marshalErr = json.Marshal(dynamicSecretResp.Secret) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) - break - } - secretValue = string(jsonBytes) - - case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: - getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) - getRotatedSecretValue.SetToken(akeylessSecretStore.token) - secretRespMap, _, apiErr := akeylessSecretStore.v2.GetRotatedSecretValue(context.Background()).Body(*getRotatedSecretValue).Execute() - if apiErr != nil { - err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) - break - } - - // assert type of secretRespMap to RotatedSecretResponse - var rotatedSecretResp RotatedSecretResponse - jsonBytes, marshalErr := json.Marshal(secretRespMap) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) - break - } - if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &rotatedSecretResp); unmarshalErr != nil { - err = fmt.Errorf("failed to unmarshal secret response to RotatedSecretResponse: %w", unmarshalErr) - break - } - - // take only relevant fields (Username and Password) from response and marshal it to a JSON string - rotatedSecretResp.Value.ApplicationID = "" - jsonBytes, marshalErr = json.Marshal(rotatedSecretResp.Value) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) - break - } - secretValue = string(jsonBytes) - } - - return secretValue, err -} - type DynamicSecretResponse struct { ID string `json:"id"` Msg string `json:"msg"` @@ -286,7 +97,7 @@ type DynamicSecretSecret struct { DisplayName string `json:"displayName"` EndDateTime string `json:"endDateTime,omitempty"` KeyID string `json:"keyId,omitempty"` - SecretText string `json:"secretText,omitempty"` + SecretText string `json:"secretText"` TenantID string `json:"tenantId,omitempty"` } @@ -307,3 +118,54 @@ func GetDaprSingleSecretResponse(secretName string, secretValue string) (secrets }, }, nil } + +func GetItemNames(items []akeyless.Item) []string { + itemNames := []string{} + for _, item := range items { + itemNames = append(itemNames, *item.ItemName) + } + return itemNames +} + +func boolToInt(b bool) int { + if b { + return 1 + } + return 0 +} + +func stringifyStaticSecret(secretValue any, secretName string) (string, error) { + var err error + + switch valueType := secretValue.(type) { + case string: + secretValue = string(valueType) + case map[string]string: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response for secret '%s': %w", secretName, marshalErr) + } else { + secretValue = string(encoded) + } + case any: + encoded, marshalErr := json.Marshal(valueType) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal secret response for secret '%s': %w", secretName, marshalErr) + break + } else { + secretValue = string(encoded) + break + } + + default: + err = fmt.Errorf("failed to assert type of secret response to string for secret '%s'", secretName) + } + + return string(secretValue.(string)), err +} + +type secretResultCollection struct { + name string + value string + err error +} From b56ac129f4f95190f84986e38c54ab1f2c3ce862 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 16:35:46 -0400 Subject: [PATCH 12/28] wip: fix get bulk secret ut Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 16 +-- secretstores/akeyless/akeyless_test.go | 167 ++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 13 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index d13db7d660..26b112b43a 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -142,7 +142,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore defer wg.Done() if len(staticItemNames) == 1 { staticSecretName := staticItemNames[0] - value, err := a.GetSingleSecretValue(staticSecretName, AKEYLESS_SECRET_TYPE_STATIC) + value, err := a.GetSingleSecretValue(staticSecretName, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} } else { @@ -163,7 +163,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range dynamicItemNames { - value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_DYNAMIC) + value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -177,7 +177,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range rotatedItemNames { - value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_ROTATED) + value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -431,6 +431,7 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) // Execute the list items request + a.logger.Debug("listing items from path '%s'...", path) itemsList, _, err := a.v2.ListItems(context.Background()).Body(*listItems).Execute() if err != nil { return nil, err @@ -518,13 +519,12 @@ func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akey continue } - if itemType == AKEYLESS_SECRET_TYPE_STATIC { + switch itemType { + case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: staticItems = append(staticItems, item) - } - if itemType == AKEYLESS_SECRET_TYPE_DYNAMIC { + case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: dynamicItems = append(dynamicItems, item) - } - if itemType == AKEYLESS_SECRET_TYPE_ROTATED { + case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: rotatedItems = append(rotatedItems, item) } } diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index b606dbe8d1..5034e084e2 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -34,26 +35,55 @@ const ( ) var ( - mockDescribeStaticSecretName = "/path/to/akeyless/static-secret-test" + mockStaticSecretItem = "/static-secret-test" + mockStaticSecretJSONItemName = "/static-secret-json-test" + mockStaticSecretPasswordItemName = "/static-secret-password-test" + mockDynamicSecretItemName = "/dynamic-secret-test" + mockRotatedSecretItemName = "/rotated-secret-test" + mockDescribeStaticItemResponse = akeyless.Item{ + ItemName: &mockStaticSecretItem, + ItemType: &mockDescribeStaticSecretType, + } + mockDescribeStaticJSONItemResponse = akeyless.Item{ + ItemName: &mockStaticSecretJSONItemName, + ItemType: &mockDescribeStaticSecretType, + } + mockDescribeStaticPasswordItemResponse = akeyless.Item{ + ItemName: &mockStaticSecretPasswordItemName, + ItemType: &mockDescribeStaticSecretType, + } + mockDescribeDynamicItemResponse = akeyless.Item{ + ItemName: &mockDynamicSecretItemName, + ItemType: &mockDescribeDynamicSecretType, + } + mockDescribeRotatedItemResponse = akeyless.Item{ + ItemName: &mockRotatedSecretItemName, + ItemType: &mockDescribeRotatedSecretType, + } + mockDescribeStaticSecretName = fmt.Sprintf("/path/to/akeyless/%s", mockStaticSecretItem) mockDescribeStaticSecretType = AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE mockDescribeStaticSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeStaticSecretName, ItemType: &mockDescribeStaticSecretType, } - mockStaticSecretJSONName = "/path/to/akeyless/static-secret-json-test" + mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless/%s", mockStaticSecretJSONItemName) mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ mockStaticSecretJSONName: { "some": "json", }, } - mockStaticSecretPasswordName = "/path/to/akeyless/static-secret-password-test" + mockStaticSecretJSONItemResponse = akeyless.Item{ + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + } + mockStaticSecretPasswordName = fmt.Sprintf("/path/to/akeyless/%s", mockStaticSecretPasswordItemName) mockGetSingleSecretPasswordValueResponse = map[string]map[string]string{ mockStaticSecretPasswordName: { "password": testSecretValue, "username": "akeyless", }, } - mockDescribeDynamicSecretName = "/path/to/akeyless/dynamic-secret-test" + mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless/%s", mockDynamicSecretItemName) mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE mockDescribeDynamicSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeDynamicSecretName, @@ -72,7 +102,7 @@ var ( }, TTLInMinutes: "60", } - mockDescribeRotatedSecretName = "/path/to/akeyless/rotated-secret-test" + mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless/%s", mockRotatedSecretItemName) mockDescribeRotatedSecretType = AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE mockDescribeRotatedSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeRotatedSecretName, @@ -740,3 +770,130 @@ func TestGetSingleRotatedSecret(t *testing.T) { mockGateway.Close() } + +func TestGetBulkSecretValues(t *testing.T) { + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-secret-value": + secretValue := map[string]string{ + mockStaticSecretItem: testSecretValue, + mockStaticSecretJSONItemName: "{\"some\":\"json\"}", + } + jsonResponse, _ := json.Marshal(&secretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/list-items": + items := akeyless.NewListItemsInPathOutput() + items.SetItems( + []akeyless.Item{ + mockDescribeStaticItemResponse, + mockDescribeStaticJSONItemResponse, + mockDescribeDynamicItemResponse, + mockDescribeRotatedItemResponse, + }, + ) + jsonResponse, _ := json.Marshal(&items) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + // Single dynamic secret value + case "/get-dynamic-secret-value": + var dynamicSecretValue DynamicSecretResponse + dynamicSecretValue.Secret.SecretText = testSecretValue + dynamicSecretValue.Secret.DisplayName = "tmp.p-1234567890.GV7LR" + jsonResponse, _ := json.Marshal(&dynamicSecretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-rotated-secret-value": + var rotatedSecretValue RotatedSecretResponse + rotatedSecretValue.Value.Username = "abcdefghijklmnopqrstuvwxyz" + rotatedSecretValue.Value.Password = testSecretValue + jsonResponse, _ := json.Marshal(&rotatedSecretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/describe-item": + + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var describeItemRequest akeyless.DescribeItem + if err := json.Unmarshal(body, &describeItemRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + var itemResponse akeyless.Item + switch describeItemRequest.Name { + case mockStaticSecretItem: + itemResponse = mockDescribeStaticItemResponse + case mockStaticSecretJSONItemName: + itemResponse = mockDescribeStaticJSONItemResponse + case mockDynamicSecretItemName: + itemResponse = mockDescribeDynamicItemResponse + case mockRotatedSecretItemName: + itemResponse = mockDescribeRotatedItemResponse + default: + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "invalid item name"}`)) + return + } + + jsonResponse, _ := json.Marshal(&itemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + assert.NotNil(t, response.Data) + assert.Contains(t, response.Data, mockStaticSecretItem) + assert.Equal(t, "{\"some\":\"json\"}", response.Data[mockStaticSecretJSONName]) + assert.Contains(t, response.Data, mockDynamicSecretItemName) + assert.Equal(t, "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}", response.Data[mockDynamicSecretItemName]) + assert.Contains(t, response.Data, mockRotatedSecretItemName) + assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", response.Data[mockDescribeRotatedSecretName]) + + mockGateway.Close() +} + +// TestGetBulkSecretValuesRecursively From 2cbffc0059030141090f23f0bbbd9f6cddddeb83 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 16:47:13 -0400 Subject: [PATCH 13/28] fix get bulk secret ut Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless_test.go | 73 ++++++++++++++++---------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 5034e084e2..b529734010 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -60,13 +60,13 @@ var ( ItemName: &mockRotatedSecretItemName, ItemType: &mockDescribeRotatedSecretType, } - mockDescribeStaticSecretName = fmt.Sprintf("/path/to/akeyless/%s", mockStaticSecretItem) + mockDescribeStaticSecretName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretItem) mockDescribeStaticSecretType = AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE mockDescribeStaticSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeStaticSecretName, ItemType: &mockDescribeStaticSecretType, } - mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless/%s", mockStaticSecretJSONItemName) + mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretJSONItemName) mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ mockStaticSecretJSONName: { "some": "json", @@ -76,14 +76,14 @@ var ( ItemName: &mockStaticSecretJSONName, ItemType: &mockDescribeStaticSecretType, } - mockStaticSecretPasswordName = fmt.Sprintf("/path/to/akeyless/%s", mockStaticSecretPasswordItemName) + mockStaticSecretPasswordName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretPasswordItemName) mockGetSingleSecretPasswordValueResponse = map[string]map[string]string{ mockStaticSecretPasswordName: { "password": testSecretValue, "username": "akeyless", }, } - mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless/%s", mockDynamicSecretItemName) + mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE mockDescribeDynamicSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeDynamicSecretName, @@ -102,7 +102,7 @@ var ( }, TTLInMinutes: "60", } - mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless/%s", mockRotatedSecretItemName) + mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless%s", mockRotatedSecretItemName) mockDescribeRotatedSecretType = AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE mockDescribeRotatedSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeRotatedSecretName, @@ -800,10 +800,10 @@ func TestGetBulkSecretValues(t *testing.T) { items := akeyless.NewListItemsInPathOutput() items.SetItems( []akeyless.Item{ - mockDescribeStaticItemResponse, - mockDescribeStaticJSONItemResponse, - mockDescribeDynamicItemResponse, - mockDescribeRotatedItemResponse, + mockDescribeStaticSecretItemResponse, + mockStaticSecretJSONItemResponse, + mockDescribeDynamicSecretItemResponse, + mockDescribeRotatedSecretItemResponse, }, ) jsonResponse, _ := json.Marshal(&items) @@ -811,23 +811,16 @@ func TestGetBulkSecretValues(t *testing.T) { w.Write(jsonResponse) // Single dynamic secret value case "/get-dynamic-secret-value": - var dynamicSecretValue DynamicSecretResponse - dynamicSecretValue.Secret.SecretText = testSecretValue - dynamicSecretValue.Secret.DisplayName = "tmp.p-1234567890.GV7LR" - jsonResponse, _ := json.Marshal(&dynamicSecretValue) + jsonResponse, _ := json.Marshal(&mockGetSingleDynamicSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) case "/get-rotated-secret-value": - var rotatedSecretValue RotatedSecretResponse - rotatedSecretValue.Value.Username = "abcdefghijklmnopqrstuvwxyz" - rotatedSecretValue.Value.Password = testSecretValue - jsonResponse, _ := json.Marshal(&rotatedSecretValue) + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) case "/describe-item": - body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -844,13 +837,13 @@ func TestGetBulkSecretValues(t *testing.T) { var itemResponse akeyless.Item switch describeItemRequest.Name { - case mockStaticSecretItem: + case "/path/to/akeyless/static-secret-test": itemResponse = mockDescribeStaticItemResponse - case mockStaticSecretJSONItemName: + case "/path/to/akeyless/static-secret-json-test": itemResponse = mockDescribeStaticJSONItemResponse - case mockDynamicSecretItemName: + case "/path/to/akeyless/dynamic-secret-test": itemResponse = mockDescribeDynamicItemResponse - case mockRotatedSecretItemName: + case "/path/to/akeyless/rotated-secret-test": itemResponse = mockDescribeRotatedItemResponse default: w.WriteHeader(http.StatusInternalServerError) @@ -886,12 +879,36 @@ func TestGetBulkSecretValues(t *testing.T) { response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) require.NoError(t, err) assert.NotNil(t, response.Data) - assert.Contains(t, response.Data, mockStaticSecretItem) - assert.Equal(t, "{\"some\":\"json\"}", response.Data[mockStaticSecretJSONName]) - assert.Contains(t, response.Data, mockDynamicSecretItemName) - assert.Equal(t, "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}", response.Data[mockDynamicSecretItemName]) - assert.Contains(t, response.Data, mockRotatedSecretItemName) - assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", response.Data[mockDescribeRotatedSecretName]) + + // Check that we got all 4 secrets (excluding any empty keys) + nonEmptySecrets := 0 + for key, value := range response.Data { + if key != "" && len(value) > 0 { + nonEmptySecrets++ + } + } + assert.Equal(t, 4, nonEmptySecrets) + + // Check static secret (text) - using the actual key from the response + staticSecretKey := "/static-secret-test" + assert.Contains(t, response.Data, staticSecretKey) + assert.Equal(t, testSecretValue, response.Data[staticSecretKey][staticSecretKey]) + + // Check static secret (JSON) + jsonSecretKey := "/static-secret-json-test" + assert.Contains(t, response.Data, jsonSecretKey) + assert.Equal(t, "{\"some\":\"json\"}", response.Data[jsonSecretKey][jsonSecretKey]) + + // Check dynamic secret + dynamicSecretKey := "/path/to/akeyless/dynamic-secret-test" + assert.Contains(t, response.Data, dynamicSecretKey) + expectedDynamicValue := "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}" + assert.Equal(t, expectedDynamicValue, response.Data[dynamicSecretKey][dynamicSecretKey]) + + // Check rotated secret + rotatedSecretKey := "/path/to/akeyless/rotated-secret-test" + assert.Contains(t, response.Data, rotatedSecretKey) + assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", response.Data[rotatedSecretKey][rotatedSecretKey]) mockGateway.Close() } From 2c0b13d13173ba2300c362d455e32cf1164da518 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 16:51:25 -0400 Subject: [PATCH 14/28] use item type from list items instead of calling describe item again Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 5 +--- secretstores/akeyless/akeyless_test.go | 36 -------------------------- 2 files changed, 1 insertion(+), 40 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 26b112b43a..5318601460 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -514,10 +514,7 @@ func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akey dynamicItems := []akeyless.Item{} rotatedItems := []akeyless.Item{} for _, item := range items { - itemType, err := a.GetSecretType(*item.ItemName) - if err != nil { - continue - } + itemType := *item.ItemType switch itemType { case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index b529734010..2d0ac90f8c 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/http/httptest" "os" @@ -820,41 +819,6 @@ func TestGetBulkSecretValues(t *testing.T) { w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - case "/describe-item": - body, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(`{"message": "failed to read request body"}`)) - return - } - - var describeItemRequest akeyless.DescribeItem - if err := json.Unmarshal(body, &describeItemRequest); err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(`{"message": "failed to parse request body"}`)) - return - } - - var itemResponse akeyless.Item - switch describeItemRequest.Name { - case "/path/to/akeyless/static-secret-test": - itemResponse = mockDescribeStaticItemResponse - case "/path/to/akeyless/static-secret-json-test": - itemResponse = mockDescribeStaticJSONItemResponse - case "/path/to/akeyless/dynamic-secret-test": - itemResponse = mockDescribeDynamicItemResponse - case "/path/to/akeyless/rotated-secret-test": - itemResponse = mockDescribeRotatedItemResponse - default: - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(`{"message": "invalid item name"}`)) - return - } - - jsonResponse, _ := json.Marshal(&itemResponse) - w.WriteHeader(http.StatusOK) - w.Write(jsonResponse) - default: // Default response for any other endpoint w.WriteHeader(http.StatusOK) From a661a98e84daf2c7870cd195bee523e5ffb8ab33 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Thu, 2 Oct 2025 17:03:44 -0400 Subject: [PATCH 15/28] added test for recursivity Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless_test.go | 277 +++++++++++++++++++++++++ 1 file changed, 277 insertions(+) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 2d0ac90f8c..a8a7240fe5 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "os" @@ -877,4 +878,280 @@ func TestGetBulkSecretValues(t *testing.T) { mockGateway.Close() } +func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { + // Test recursive secret retrieval from different hierarchical paths + // This test simulates a folder structure where: + // - Root "/" contains 4 subfolders + // - Each subfolder contains different types of secrets + // - The listItemsRecursively method should traverse all folders + + // Define mock secrets for different paths + staticSecret1 := "/path/to/static/secrets/secret1" + staticSecret2 := "/path/to/static/secrets/secret2" + staticSecret3 := "/path/to/static/secrets/secret3" + dynamicSecret1 := "/path/to/dynamic/secrets/dynamic1" + dynamicSecret2 := "/path/to/dynamic/secrets/dynamic2" + rotatedSecret1 := "/path/to/rotated/secrets/rotated1" + mixedStaticSecret := "/path/to/mixed/secrets/mixed-static" + mixedDynamicSecret := "/path/to/mixed/secrets/mixed-dynamic" + mixedRotatedSecret := "/path/to/mixed/secrets/mixed-rotated" + + // Create mock items for different paths + staticItem1 := akeyless.Item{ + ItemName: &staticSecret1, + ItemType: &mockDescribeStaticSecretType, + } + staticItem2 := akeyless.Item{ + ItemName: &staticSecret2, + ItemType: &mockDescribeStaticSecretType, + } + staticItem3 := akeyless.Item{ + ItemName: &staticSecret3, + ItemType: &mockDescribeStaticSecretType, + } + dynamicItem1 := akeyless.Item{ + ItemName: &dynamicSecret1, + ItemType: &mockDescribeDynamicSecretType, + } + dynamicItem2 := akeyless.Item{ + ItemName: &dynamicSecret2, + ItemType: &mockDescribeDynamicSecretType, + } + rotatedItem1 := akeyless.Item{ + ItemName: &rotatedSecret1, + ItemType: &mockDescribeRotatedSecretType, + } + mixedStaticItem := akeyless.Item{ + ItemName: &mixedStaticSecret, + ItemType: &mockDescribeStaticSecretType, + } + mixedDynamicItem := akeyless.Item{ + ItemName: &mixedDynamicSecret, + ItemType: &mockDescribeDynamicSecretType, + } + mixedRotatedItem := akeyless.Item{ + ItemName: &mixedRotatedSecret, + ItemType: &mockDescribeRotatedSecretType, + } + + var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + // Handle different endpoints + switch r.URL.Path { + case "/auth": + // Return a proper AuthOutput JSON response for authentication + authOutput := akeyless.NewAuthOutput() + authOutput.SetToken("t-1234567890") + authOutput.SetExpiration("2025-01-01T00:00:00Z") + jsonResponse, _ := json.Marshal(authOutput) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-secret-value": + secretValue := map[string]string{ + staticSecret1: testSecretValue, + staticSecret2: "static-secret-2-value", + staticSecret3: "static-secret-3-value", + mixedStaticSecret: "mixed-static-secret-value", + } + jsonResponse, _ := json.Marshal(&secretValue) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/list-items": + // Parse the path from request body to determine what to return + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var listItemsRequest akeyless.ListItems + if err := json.Unmarshal(body, &listItemsRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + path := "" + if listItemsRequest.Path != nil { + path = *listItemsRequest.Path + } + // Debug: Uncomment to see recursive calls + // fmt.Printf("DEBUG: list-items called for path: '%s'\n", path) + + var items akeyless.ListItemsInPathOutput + + switch path { + case "/": + // Root path returns only folders, no items + folders := []string{ + "/path/to/static/secrets", + "/path/to/dynamic/secrets", + "/path/to/rotated/secrets", + "/path/to/mixed/secrets", + } + items.SetFolders(folders) + items.SetItems([]akeyless.Item{}) + + case "/path/to/static/secrets": + // Static secrets folder + items.SetItems([]akeyless.Item{staticItem1, staticItem2, staticItem3}) + items.SetFolders([]string{}) + + case "/path/to/dynamic/secrets": + // Dynamic secrets folder + items.SetItems([]akeyless.Item{dynamicItem1, dynamicItem2}) + items.SetFolders([]string{}) + + case "/path/to/rotated/secrets": + // Rotated secrets folder + items.SetItems([]akeyless.Item{rotatedItem1}) + items.SetFolders([]string{}) + + case "/path/to/mixed/secrets": + // Mixed secrets folder + items.SetItems([]akeyless.Item{mixedStaticItem, mixedDynamicItem, mixedRotatedItem}) + items.SetFolders([]string{}) + + default: + // Unknown path + items.SetItems([]akeyless.Item{}) + items.SetFolders([]string{}) + } + + jsonResponse, _ := json.Marshal(&items) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-dynamic-secret-value": + // Create dynamic secret responses for each secret + var dynamicSecretResponse DynamicSecretResponse + dynamicSecretResponse.Secret.SecretText = "dynamic-secret-1-value" + dynamicSecretResponse.Secret.DisplayName = "dynamic-secret-1" + jsonResponse, _ := json.Marshal(&dynamicSecretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/get-rotated-secret-value": + // Create rotated secret response + var rotatedSecretResponse RotatedSecretResponse + rotatedSecretResponse.Value.Username = "rotated-user" + rotatedSecretResponse.Value.Password = "rotated-secret-1-value" + jsonResponse, _ := json.Marshal(&rotatedSecretResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + case "/describe-item": + body, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to read request body"}`)) + return + } + + var describeItemRequest akeyless.DescribeItem + if err := json.Unmarshal(body, &describeItemRequest); err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "failed to parse request body"}`)) + return + } + + var itemResponse akeyless.Item + switch describeItemRequest.Name { + case staticSecret1, staticSecret2, staticSecret3, mixedStaticSecret: + itemResponse = akeyless.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeStaticSecretType, + } + case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: + itemResponse = akeyless.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeDynamicSecretType, + } + case rotatedSecret1, mixedRotatedSecret: + itemResponse = akeyless.Item{ + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeRotatedSecretType, + } + default: + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"message": "invalid item name"}`)) + return + } + + jsonResponse, _ := json.Marshal(&itemResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + + default: + // Default response for any other endpoint + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"message": "mock response"}`)) + } + })) + + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + err := store.Init(context.Background(), meta) + require.NoError(t, err) + + response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) + require.NoError(t, err) + assert.NotNil(t, response.Data) + + // Check that we got all 9 secrets (4 static, 3 dynamic, 2 rotated) + nonEmptySecrets := 0 + for key, value := range response.Data { + if key != "" && len(value) > 0 { + nonEmptySecrets++ + } + } + assert.Equal(t, 9, nonEmptySecrets) + + // Check static secrets from /path/to/static/secrets + assert.Contains(t, response.Data, staticSecret1) + assert.Equal(t, testSecretValue, response.Data[staticSecret1][staticSecret1]) + assert.Contains(t, response.Data, staticSecret2) + assert.Equal(t, "static-secret-2-value", response.Data[staticSecret2][staticSecret2]) + assert.Contains(t, response.Data, staticSecret3) + assert.Equal(t, "static-secret-3-value", response.Data[staticSecret3][staticSecret3]) + + // Check dynamic secrets from /path/to/dynamic/secrets + assert.Contains(t, response.Data, dynamicSecret1) + expectedDynamicValue1 := "{\"displayName\":\"dynamic-secret-1\",\"secretText\":\"dynamic-secret-1-value\"}" + assert.Equal(t, expectedDynamicValue1, response.Data[dynamicSecret1][dynamicSecret1]) + assert.Contains(t, response.Data, dynamicSecret2) + expectedDynamicValue2 := "{\"displayName\":\"dynamic-secret-1\",\"secretText\":\"dynamic-secret-1-value\"}" + assert.Equal(t, expectedDynamicValue2, response.Data[dynamicSecret2][dynamicSecret2]) + + // Check rotated secret from /path/to/rotated/secrets + assert.Contains(t, response.Data, rotatedSecret1) + expectedRotatedValue1 := "{\"username\":\"rotated-user\",\"password\":\"rotated-secret-1-value\"}" + assert.Equal(t, expectedRotatedValue1, response.Data[rotatedSecret1][rotatedSecret1]) + + // Check mixed secrets from /path/to/mixed/secrets + assert.Contains(t, response.Data, mixedStaticSecret) + assert.Equal(t, "mixed-static-secret-value", response.Data[mixedStaticSecret][mixedStaticSecret]) + assert.Contains(t, response.Data, mixedDynamicSecret) + expectedMixedDynamicValue := "{\"displayName\":\"dynamic-secret-1\",\"secretText\":\"dynamic-secret-1-value\"}" + assert.Equal(t, expectedMixedDynamicValue, response.Data[mixedDynamicSecret][mixedDynamicSecret]) + assert.Contains(t, response.Data, mixedRotatedSecret) + expectedMixedRotatedValue := "{\"username\":\"rotated-user\",\"password\":\"rotated-secret-1-value\"}" + assert.Equal(t, expectedMixedRotatedValue, response.Data[mixedRotatedSecret][mixedRotatedSecret]) + + mockGateway.Close() +} + // TestGetBulkSecretValuesRecursively From d5eefe6a5fafcc9402a35644df1aa97f0b754031 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 3 Oct 2025 08:58:54 -0400 Subject: [PATCH 16/28] mv isactive func to utils Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 58 ++++++----- secretstores/akeyless/akeyless_test.go | 130 ++++++++++++++----------- secretstores/akeyless/utils.go | 61 ++++++++++++ 3 files changed, 170 insertions(+), 79 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 5318601460..fd20a73ad8 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -46,7 +46,6 @@ type akeylessMetadata struct { // with authentication method based on the accessId. func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metadata) error { a.logger.Info("Initializing Akeyless secret store...") - a.logger.Info("Parsing metadata...") m, err := a.parseMetadata(meta) if err != nil { return errors.New("failed to parse metadata: " + err.Error()) @@ -66,19 +65,19 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") } - a.logger.Debug("getting secret type for '%s'...", req.Name) + a.logger.Debugf("getting secret type for '%s'...", req.Name) secretType, err := a.GetSecretType(req.Name) if err != nil { return secretstores.GetSecretResponse{}, err } - a.logger.Debug("getting secret value for '%s' (type %s)...", req.Name, secretType) + a.logger.Debugf("getting secret value for '%s' (type %s)...", req.Name, secretType) secretValue, err := a.GetSingleSecretValue(req.Name, secretType) if err != nil { return secretstores.GetSecretResponse{}, errors.New(err.Error()) } - a.logger.Debug("secret '%s' value: %s", req.Name, secretValue[:3]+"[REDACTED]") + a.logger.Debugf("secret '%s' value: %s", req.Name, secretValue[:3]+"[REDACTED]") // Return the secret in the expected format return GetDaprSingleSecretResponse(req.Name, secretValue) @@ -87,8 +86,9 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge // BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values. // The method performs the following steps: // 1. Recursively list all items in Akeyless -// 2. Separate items by type since only static secrets are supported for bulk get -// 3. Get secret values concurrently, each item type in a separate goroutine +// 2. Filter out inactive/failing secrets +// 3. Separate items by type since only static secrets are supported for bulk get +// 4. Get secret values concurrently, each item type in a separate goroutine func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) { if a.v2 == nil { return secretstores.BulkGetSecretResponse{}, errors.New("akeyless client not initialized") @@ -112,9 +112,14 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore return response, nil } + // filter out inactive secrets + a.logger.Debugf("filtering out inactive secrets, %d items before filtering", len(listItems)) + listItems = a.filterInactiveSecrets(listItems) + a.logger.Debugf("filtering out inactive secrets, %d items after filtering", len(listItems)) + // separate items by type since only static secrets are supported for bulk get staticItems, dynamicItems, rotatedItems := a.separateItemsByType(listItems) - a.logger.Info("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItems), len(dynamicItems), len(rotatedItems)) + a.logger.Infof("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItems), len(dynamicItems), len(rotatedItems)) // listItems can get quite large, so we don't need all item details, we can use the item names instead // and free memory @@ -122,9 +127,9 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore staticItemNames := GetItemNames(staticItems) dynamicItemNames := GetItemNames(dynamicItems) rotatedItemNames := GetItemNames(rotatedItems) - a.logger.Debug("static items: %v", staticItemNames) - a.logger.Debug("dynamic items: %v", dynamicItemNames) - a.logger.Debug("rotated items: %v", rotatedItemNames) + a.logger.Debugf("static items: %v", staticItemNames) + a.logger.Debugf("dynamic items: %v", dynamicItemNames) + a.logger.Debugf("rotated items: %v", rotatedItemNames) haveStaticItems := len(staticItemNames) > 0 haveDynamicItems := len(dynamicItemNames) > 0 @@ -143,11 +148,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore if len(staticItemNames) == 1 { staticSecretName := staticItemNames[0] value, err := a.GetSingleSecretValue(staticSecretName, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE) - if err != nil { - secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} - } else { - secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: nil} - } + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} } else { secretResponse := a.GetBulkStaticSecretValues(staticItemNames) if len(secretResponse) > 0 { @@ -196,7 +197,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore // collect results and populate response for result := range secretResultChannels { if result.err != nil { - a.logger.Error("error getting secret '%s': %s. Skipping...", result.name, result.err.Error()) + a.logger.Errorf("error getting secret '%s': %s. Skipping...", result.name, result.err.Error()) continue } @@ -254,12 +255,12 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle return nil, errors.New("unable to extract access type character from accessId, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") } - a.logger.Debug("getting access type display name for character %s...", accessTypeChar) + a.logger.Debugf("getting access type display name for character %s...", accessTypeChar) accessTypeDisplayName, err := GetAccessTypeDisplayName(accessTypeChar) if err != nil { return nil, errors.New("unable to get access type display name, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") } - a.logger.Debug("access type detected: %s", accessTypeDisplayName) + a.logger.Debugf("access type detected: %s", accessTypeDisplayName) switch accessTypeDisplayName { case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: @@ -275,7 +276,7 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle // Set default gateway URL if not specified if m.GatewayURL == "" { - a.logger.Info("Gateway URL is not set, using default value %s...", AKEYLESS_PUBLIC_GATEWAY_URL) + a.logger.Infof("Gateway URL is not set, using default value %s...", AKEYLESS_PUBLIC_GATEWAY_URL) m.GatewayURL = AKEYLESS_PUBLIC_GATEWAY_URL } @@ -431,7 +432,7 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) // Execute the list items request - a.logger.Debug("listing items from path '%s'...", path) + a.logger.Debugf("listing items from path '%s'...", path) itemsList, _, err := a.v2.ListItems(context.Background()).Body(*listItems).Execute() if err != nil { return nil, err @@ -502,8 +503,8 @@ func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { return fmt.Errorf("failed to authenticate with Akeyless: %w", err) } - a.logger.Debug("setting token %s for authentication...", out.GetToken()[:3]+"[REDACTED]") - a.logger.Debug("expires at: %s", out.GetExpiration()) + a.logger.Debugf("setting token %s for authentication...", out.GetToken()[:3]+"[REDACTED]") + a.logger.Debugf("expires at: %s", out.GetExpiration()) a.token = out.GetToken() return nil @@ -527,3 +528,16 @@ func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akey } return staticItems, dynamicItems, rotatedItems } + +func (a *akeylessSecretStore) filterInactiveSecrets(secrets []akeyless.Item) []akeyless.Item { + + filteredSecrets := []akeyless.Item{} + + for _, secret := range secrets { + if isSecretActive(secret, a.logger) { + filteredSecrets = append(filteredSecrets, secret) + } + } + + return filteredSecrets +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index a8a7240fe5..fe7d4cdf37 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -35,36 +35,17 @@ const ( ) var ( - mockStaticSecretItem = "/static-secret-test" - mockStaticSecretJSONItemName = "/static-secret-json-test" - mockStaticSecretPasswordItemName = "/static-secret-password-test" - mockDynamicSecretItemName = "/dynamic-secret-test" - mockRotatedSecretItemName = "/rotated-secret-test" - mockDescribeStaticItemResponse = akeyless.Item{ - ItemName: &mockStaticSecretItem, - ItemType: &mockDescribeStaticSecretType, - } - mockDescribeStaticJSONItemResponse = akeyless.Item{ - ItemName: &mockStaticSecretJSONItemName, - ItemType: &mockDescribeStaticSecretType, - } - mockDescribeStaticPasswordItemResponse = akeyless.Item{ - ItemName: &mockStaticSecretPasswordItemName, - ItemType: &mockDescribeStaticSecretType, - } - mockDescribeDynamicItemResponse = akeyless.Item{ - ItemName: &mockDynamicSecretItemName, - ItemType: &mockDescribeDynamicSecretType, - } - mockDescribeRotatedItemResponse = akeyless.Item{ - ItemName: &mockRotatedSecretItemName, - ItemType: &mockDescribeRotatedSecretType, - } + mockStaticSecretItem = "/static-secret-test" + mockStaticSecretJSONItemName = "/static-secret-json-test" + mockStaticSecretPasswordItemName = "/static-secret-password-test" + mockDynamicSecretItemName = "/dynamic-secret-test" + mockRotatedSecretItemName = "/rotated-secret-test" mockDescribeStaticSecretName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretItem) mockDescribeStaticSecretType = AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE mockDescribeStaticSecretItemResponse = akeyless.Item{ - ItemName: &mockDescribeStaticSecretName, - ItemType: &mockDescribeStaticSecretType, + ItemName: &mockDescribeStaticSecretName, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretJSONItemName) mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ @@ -73,8 +54,9 @@ var ( }, } mockStaticSecretJSONItemResponse = akeyless.Item{ - ItemName: &mockStaticSecretJSONName, - ItemType: &mockDescribeStaticSecretType, + ItemName: &mockStaticSecretJSONName, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } mockStaticSecretPasswordName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretPasswordItemName) mockGetSingleSecretPasswordValueResponse = map[string]map[string]string{ @@ -86,8 +68,14 @@ var ( mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE mockDescribeDynamicSecretItemResponse = akeyless.Item{ - ItemName: &mockDescribeDynamicSecretName, - ItemType: &mockDescribeDynamicSecretType, + ItemName: &mockDescribeDynamicSecretName, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + DynamicSecretProducerDetails: &akeyless.DynamicSecretProducerInfo{ + ProducerStatus: func(s string) *string { return &s }("ProducerConnected"), + }, + }, } mockGetSingleDynamicSecretValueResponse = DynamicSecretResponse{ ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", @@ -105,8 +93,14 @@ var ( mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless%s", mockRotatedSecretItemName) mockDescribeRotatedSecretType = AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE mockDescribeRotatedSecretItemResponse = akeyless.Item{ - ItemName: &mockDescribeRotatedSecretName, - ItemType: &mockDescribeRotatedSecretType, + ItemName: &mockDescribeRotatedSecretName, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + RotatedSecretDetails: &akeyless.RotatedSecretDetailsInfo{ + RotatorStatus: func(s string) *string { return &s }("RotationSucceeded"), + }, + }, } mockGetSingleRotatedSecretValueResponse = RotatedSecretResponse{ Value: RotatedSecretValue{ @@ -898,40 +892,49 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { // Create mock items for different paths staticItem1 := akeyless.Item{ - ItemName: &staticSecret1, - ItemType: &mockDescribeStaticSecretType, + ItemName: &staticSecret1, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } staticItem2 := akeyless.Item{ - ItemName: &staticSecret2, - ItemType: &mockDescribeStaticSecretType, + ItemName: &staticSecret2, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } staticItem3 := akeyless.Item{ - ItemName: &staticSecret3, - ItemType: &mockDescribeStaticSecretType, + ItemName: &staticSecret3, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } dynamicItem1 := akeyless.Item{ - ItemName: &dynamicSecret1, - ItemType: &mockDescribeDynamicSecretType, + ItemName: &dynamicSecret1, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } dynamicItem2 := akeyless.Item{ - ItemName: &dynamicSecret2, - ItemType: &mockDescribeDynamicSecretType, + ItemName: &dynamicSecret2, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } rotatedItem1 := akeyless.Item{ - ItemName: &rotatedSecret1, - ItemType: &mockDescribeRotatedSecretType, + ItemName: &rotatedSecret1, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } mixedStaticItem := akeyless.Item{ - ItemName: &mixedStaticSecret, - ItemType: &mockDescribeStaticSecretType, + ItemName: &mixedStaticSecret, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } mixedDynamicItem := akeyless.Item{ - ItemName: &mixedDynamicSecret, - ItemType: &mockDescribeDynamicSecretType, + ItemName: &mixedDynamicSecret, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } mixedRotatedItem := akeyless.Item{ - ItemName: &mixedRotatedSecret, - ItemType: &mockDescribeRotatedSecretType, + ItemName: &mixedRotatedSecret, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -1063,18 +1066,31 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { switch describeItemRequest.Name { case staticSecret1, staticSecret2, staticSecret3, mixedStaticSecret: itemResponse = akeyless.Item{ - ItemName: &describeItemRequest.Name, - ItemType: &mockDescribeStaticSecretType, + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), } case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: itemResponse = akeyless.Item{ - ItemName: &describeItemRequest.Name, - ItemType: &mockDescribeDynamicSecretType, + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + DynamicSecretProducerDetails: &akeyless.DynamicSecretProducerInfo{ + ProducerStatus: func(s string) *string { return &s }("ProducerConnected"), + }, + }, } case rotatedSecret1, mixedRotatedSecret: itemResponse = akeyless.Item{ - ItemName: &describeItemRequest.Name, - ItemType: &mockDescribeRotatedSecretType, + ItemName: &describeItemRequest.Name, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + ItemGeneralInfo: &akeyless.ItemGeneralInfo{ + RotatedSecretDetails: &akeyless.RotatedSecretDetailsInfo{ + RotatorStatus: func(s string) *string { return &s }("RotationSucceeded"), + }, + }, } default: w.WriteHeader(http.StatusInternalServerError) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 5acdb380d8..4d63eb3eaf 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -9,6 +9,7 @@ import ( "github.com/akeylesslabs/akeyless-go/v5" "github.com/dapr/components-contrib/secretstores" + "github.com/dapr/kit/logger" ) // Define constants for the access types. These are equivalent to the TypeScript consts. @@ -169,3 +170,63 @@ type secretResultCollection struct { value string err error } + +func isSecretActive(secret akeyless.Item, logger logger.Logger) bool { + + var isActive bool + + // check if secret has isEnabled field + if secret.IsEnabled == nil { + logger.Debugf("secret '%s' is missing isEnabled field, skipping...", *secret.ItemName) + return false + } + + if !*secret.IsEnabled { + logger.Debugf("secret '%s' is not enabled, skipping...", *secret.ItemName) + return false + } + + switch *secret.ItemType { + case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + logger.Debugf("static secret '%s' is active", *secret.ItemName) + isActive = true + case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + // Check if ItemGeneralInfo is available, if not, include the secret + if secret.ItemGeneralInfo != nil && + secret.ItemGeneralInfo.DynamicSecretProducerDetails != nil && + secret.ItemGeneralInfo.DynamicSecretProducerDetails.ProducerStatus != nil { + status := *secret.ItemGeneralInfo.DynamicSecretProducerDetails.ProducerStatus + if status == "ProducerConnected" { + logger.Debugf("dynamic secret '%s' is active, adding to filtered secrets...", *secret.ItemName) + isActive = true + } else { + logger.Debugf("dynamic secret '%s' producer status is '%s', skipping...", *secret.ItemName, status) + } + } else { + // If detailed info is not available, include the secret + logger.Debugf("dynamic secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) + isActive = true + } + case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + // Check if ItemGeneralInfo is available, if not, include the secret + if secret.ItemGeneralInfo != nil && + secret.ItemGeneralInfo.RotatedSecretDetails != nil && + secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus != nil { + status := *secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus + if status == "RotationSucceeded" { + isActive = true + } else { + logger.Debugf("rotated secret '%s' rotation status is '%s', skipping...", *secret.ItemName, status) + } + } else { + // If detailed info is not available, include the secret + logger.Debugf("rotated secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) + isActive = true + } + default: + logger.Debugf("secret '%s' is of unsupported type '%s', skipping...", *secret.ItemName, *secret.ItemType) + isActive = false + } + + return isActive +} From e58b3e92590ffcf18983ed10301ca26868c4a635 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 3 Oct 2025 10:39:06 -0400 Subject: [PATCH 17/28] retrieve all details when listing items (for item state) Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 1 - secretstores/akeyless/akeyless_test.go | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index fd20a73ad8..4cdeb43510 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -427,7 +427,6 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item listItems := akeyless.NewListItems() listItems.SetToken(a.token) listItems.SetPath(path) - listItems.SetMinimalView(true) listItems.SetAutoPagination("enabled") listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index fe7d4cdf37..ab19b99f74 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -28,9 +28,7 @@ const ( // "name": "John Doe", // "iat": 1516239022 // } - testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" - // testDynamicSecretName = "/path/to/dynamic-secret-test" - // testRotatedSecretName = "/path/to/rotated-secret-test" + testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" testSecretValue = "r3vE4L3D" ) @@ -1169,5 +1167,3 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { mockGateway.Close() } - -// TestGetBulkSecretValuesRecursively From bbb86e4a81d9f5475dad8beaa81d010bbdf1489e Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Sat, 4 Oct 2025 00:31:24 -0400 Subject: [PATCH 18/28] added k8s auth Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 32 +++++++++++++++++++---------- secretstores/akeyless/metadata.yaml | 20 ++++++++++++++++++ secretstores/akeyless/utils.go | 25 ++++++++++++++++++++++ 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 4cdeb43510..692b2b1f8a 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -35,11 +35,14 @@ func NewAkeylessSecretStore(logger logger.Logger) secretstores.SecretStore { // akeylessMetadata contains the metadata for the Akeyless secret store. type akeylessMetadata struct { - GatewayURL string `json:"gatewayUrl" mapstructure:"gatewayUrl"` - JWT string `json:"jwt" mapstructure:"jwt"` - AccessID string `json:"accessId" mapstructure:"accessId"` - AccessKey string `json:"accessKey" mapstructure:"accessKey"` - AccessType string `json:"accessType" mapstructure:"accessType"` + GatewayURL string `json:"gatewayUrl" mapstructure:"gatewayUrl"` + JWT string `json:"jwt" mapstructure:"jwt"` + AccessID string `json:"accessId" mapstructure:"accessId"` + AccessKey string `json:"accessKey" mapstructure:"accessKey"` + AccessType string `json:"accessType" mapstructure:"accessType"` + K8SGatewayURL string `json:"k8sGatewayUrl" mapstructure:"k8sGatewayUrl"` + K8SAuthConfigName string `json:"k8sAuthConfigName" mapstructure:"k8sAuthConfigName"` + K8sServiceAccountToken string `json:"k8sServiceAccountToken" mapstructure:"k8sServiceAccountToken"` } // Init creates a new Akeyless secret store client and sets up the Akeyless API client @@ -465,22 +468,30 @@ func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { authRequest.SetAccessId(metadata.AccessID) authRequest.SetAccessType(metadata.AccessType) + var accessType = metadata.AccessType + + a.logger.Debugf("authenticating using access type: %s", accessType) + // Depending on the access type we set the appropriate authentication method - switch metadata.AccessType { + switch accessType { // If access type is AWS IAM we use the cloud ID case AKEYLESS_AUTH_ACCESS_IAM: - a.logger.Debug("getting cloud ID for AWS IAM...") id, err := aws.GetCloudId() if err != nil { return errors.New("unable to get cloud ID") } authRequest.SetCloudId(id) case AKEYLESS_AUTH_ACCESS_JWT: - a.logger.Debug("setting JWT for authentication...") authRequest.SetJwt(metadata.JWT) case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: - a.logger.Debug("setting access key for authentication...") + a.logger.Debug("authenticating using access key...") authRequest.SetAccessKey(metadata.AccessKey) + case AKEYLESS_AUTH_ACCESS_K8S: + a.logger.Debug("authenticating using k8s...") + err := setK8SAuthConfiguration(*metadata, authRequest, a) + if err != nil { + return fmt.Errorf("failed to set k8s auth configuration: %w", err) + } } // Create Akeyless API client configuration @@ -502,8 +513,7 @@ func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { return fmt.Errorf("failed to authenticate with Akeyless: %w", err) } - a.logger.Debugf("setting token %s for authentication...", out.GetToken()[:3]+"[REDACTED]") - a.logger.Debugf("expires at: %s", out.GetExpiration()) + a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) a.token = out.GetToken() return nil diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml index ae6f794f4c..bdeec69512 100644 --- a/secretstores/akeyless/metadata.yaml +++ b/secretstores/akeyless/metadata.yaml @@ -35,4 +35,24 @@ metadata: If using the API key (access_key) authentication method, specify it here. example: "ABCD1233...=" type: string + sensitive: true + - name: k8sAuthConfigName + required: false + description: | + If using the k8s auth method, specify the name of the k8s auth config. + example: "k8s-auth-config" + type: string + - name: k8sGatewayUrl + required: false + description: | + The gateway URL that where the k8s auth config is located. + example: "https://gw.akeyless.svc.cluster.local" + type: string + - name: k8sServiceAccountToken + required: false + description: | + If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file. + example: "eyJ..." + type: string sensitive: true \ No newline at end of file diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 4d63eb3eaf..c81ae28261 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "os" "regexp" "strings" @@ -17,6 +18,7 @@ const ( AKEYLESS_AUTH_ACCESS_JWT = "jwt" AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE = "access_key" AKEYLESS_AUTH_ACCESS_IAM = "aws_iam" + AKEYLESS_AUTH_ACCESS_K8S = "k8s" AKEYLESS_PUBLIC_GATEWAY_URL = "https://api.akeyless.io" AKEYLESS_USER_AGENT = "dapr.io/akeyless-secret-store" AKEYLESS_SECRET_TYPE_STATIC = "static-secret" @@ -32,6 +34,7 @@ var AccessTypeCharMap = map[string]string{ "a": AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE, "o": AKEYLESS_AUTH_ACCESS_JWT, "w": AKEYLESS_AUTH_ACCESS_IAM, + "k": AKEYLESS_AUTH_ACCESS_K8S, } // AccessIdRegex is the compiled regular expression for validating Akeyless Access IDs. @@ -230,3 +233,25 @@ func isSecretActive(secret akeyless.Item, logger logger.Logger) bool { return isActive } + +func setK8SAuthConfiguration(metadata akeylessMetadata, authRequest *akeyless.Auth, a *akeylessSecretStore) error { + if metadata.K8SAuthConfigName == "" { + return fmt.Errorf("k8s auth config name is required") + } + authRequest.SetK8sAuthConfigName(metadata.K8SAuthConfigName) + if metadata.K8sServiceAccountToken == "" { + a.logger.Debug("k8s service account token is missing, attempting to read from default service account token file") + token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + if err != nil { + return fmt.Errorf("failed to read default service account token file: %w", err) + } + metadata.K8sServiceAccountToken = string(token) + } + if metadata.K8SGatewayURL == "" { + a.logger.Debug("k8s gateway url is missing, using gatewayUrl") + metadata.K8SGatewayURL = metadata.GatewayURL + } + authRequest.SetGatewayUrl(metadata.K8SGatewayURL) + authRequest.SetK8sServiceAccountToken(metadata.K8sServiceAccountToken) + return nil +} From cd18d5ddceb30b2f6a2519883b4b6fc5f0ee3185 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Mon, 20 Oct 2025 13:42:56 -0400 Subject: [PATCH 19/28] added k8s auth conf to example Signed-off-by: Kobbi Gal --- secretstores/akeyless/example.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/secretstores/akeyless/example.yaml b/secretstores/akeyless/example.yaml index a43157c38b..009e70bb43 100644 --- a/secretstores/akeyless/example.yaml +++ b/secretstores/akeyless/example.yaml @@ -14,3 +14,9 @@ spec: value: "ABCD1233...=" # Optional: your Akeyless access key for API key authentication - name: jwt value: "eyJ..." # Required: your Akeyless JWT for JWT authentication + - name: k8sAuthConfigName + value: "k8s-auth-config" + - name: k8sGatewayUrl + value: "https://gw.akeyless.svc.cluster.local" + - name: k8sServiceAccountToken + value: "eyJ..." \ No newline at end of file From 55c43f475bd3489cbd2a363767da7bace27b9fac Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Mon, 20 Oct 2025 14:07:48 -0400 Subject: [PATCH 20/28] added k8s auth conf to readme Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 42 ++++++++++++++++++++++++----- secretstores/akeyless/metadata.yaml | 2 +- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index 82c9b28b24..d7e2097a59 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -4,6 +4,13 @@ This component provides a Dapr secret store implementation for [Akeyless](https: ## Configuration +- [API Key](https://docs.akeyless.io/docs/api-key) +- [OAuth2.0/JWT](https://docs.akeyless.io/docs/oauth20jwt) +- [AWS IAM](https://docs.akeyless.io/docs/aws-iam) +- [Kubernetes](https://docs.akeyless.io/docs/kubernetes-auth) + +### Authentication + The Akeyless secret store component supports the following configuration options: | Field | Required | Description | Example | @@ -12,12 +19,13 @@ The Akeyless secret store component supports the following configuration options | `accessId` | Yes | The Akeyless authentication access ID. | `p-123456780wm` | | `jwt` | No | If using an OAuth2.0/JWT access ID, specify the JSON Web Token | `eyJ...` | | `accessKey` | No | If using an API Key access ID, specify the API key | `ABCD123...=` | +| `k8sAuthConfigName` | No | If using the k8s auth method, specify the name of the k8s auth config. | `k8s-auth-config` | +| `k8sGatewayUrl` | No | The gateway URL that where the k8s auth config is located. | `http://gw.akeyless.svc.cluster.local:8000` | +| `k8sServiceAccountToken` | No | If using the k8s auth method, specify the service account token. If not specified, + we will try to read it from the default service account token file. | `eyJ...` | We currently support the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): -- [API Key](https://docs.akeyless.io/docs/api-key) -- [OAuth2.0/JWT](https://docs.akeyless.io/docs/oauth20jwt) -- [AWS IAM](https://docs.akeyless.io/docs/aws-iam) ## Example Configuration: API Key @@ -75,9 +83,30 @@ spec: value: "p-1234Abcdwm" ``` +## Example Configuration: Kubernetes + +```yaml +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: akeyless +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://gw.akeyless.svc.cluster.local" + - name: accessId + value: "p-1234Abcdwm" + - name: k8sAuthConfigName + value: "us-east-1-prod-akeyless-k8s-conf" + - name: k8sGatewayUrl + value: https://gw.akeyless.svc.cluster.local +``` + ## Usage -Once configured, you can retrieve secrets using the Dapr secrets API: +Once configured, you can retrieve secrets using the Dapr secrets API/SDK: ```bash # Get a single secret @@ -89,5 +118,6 @@ curl http://localhost:3500/v1.0/secrets/akeyless/bulk ## Features -- **GetSecret**: Retrieve an individual static secret by name. -- **BulkGetSecret**: Retrieve an all static secrets. +- Supports static, dynamic and rotated secrets. +- **GetSecret**: Retrieve an individual value secret by path. +- **BulkGetSecret**: Retrieve an all secrets from the root path. diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml index bdeec69512..d34c612187 100644 --- a/secretstores/akeyless/metadata.yaml +++ b/secretstores/akeyless/metadata.yaml @@ -46,7 +46,7 @@ metadata: required: false description: | The gateway URL that where the k8s auth config is located. - example: "https://gw.akeyless.svc.cluster.local" + example: "http://gw.akeyless.svc.cluster.local:8000" type: string - name: k8sServiceAccountToken required: false From 9751d3b8d55a656ca1f07e684d8495fb2a635329 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 09:56:14 -0400 Subject: [PATCH 21/28] rm unneeded util func treat initial rotation status as active Signed-off-by: Kobbi Gal --- secretstores/akeyless/utils.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index c81ae28261..4b1f77f60b 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -60,22 +60,6 @@ func ExtractAccessTypeChar(accessId string) (string, error) { return string(idPart[len(idPart)-2]), nil } -// validateAccessTypeChar validates the extracted access type character against a list of allowed types. -// It returns true if the extracted access type is in the allowed list, false otherwise. -func ValidateAccessTypeChar(accessId string, allowedTypes []string) bool { - typeChar, err := ExtractAccessTypeChar(accessId) - if err != nil { - return false // Invalid ID format - } - - for _, allowedType := range allowedTypes { - if typeChar == allowedType { - return true - } - } - return false -} - // getAccessTypeDisplayName gets the full display name of the access type from the character. // It returns the display name (e.g., 'api_key') or an error if the type character is unknown. func GetAccessTypeDisplayName(typeChar string) (string, error) { @@ -216,7 +200,7 @@ func isSecretActive(secret akeyless.Item, logger logger.Logger) bool { secret.ItemGeneralInfo.RotatedSecretDetails != nil && secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus != nil { status := *secret.ItemGeneralInfo.RotatedSecretDetails.RotatorStatus - if status == "RotationSucceeded" { + if status == "RotationSucceeded" || status == "RotationInitialStatus" { isActive = true } else { logger.Debugf("rotated secret '%s' rotation status is '%s', skipping...", *secret.ItemName, status) From 8f47044bd857ca542fa467188a258ea6714dfae2 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 10:17:49 -0400 Subject: [PATCH 22/28] rm example.yaml Signed-off-by: Kobbi Gal --- secretstores/akeyless/example.yaml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 secretstores/akeyless/example.yaml diff --git a/secretstores/akeyless/example.yaml b/secretstores/akeyless/example.yaml deleted file mode 100644 index 009e70bb43..0000000000 --- a/secretstores/akeyless/example.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: dapr.io/v1alpha1 -kind: Component -metadata: - name: akeyless-secretstore -spec: - type: secretstores.akeyless - version: v1 - metadata: - - name: gatewayUrl - value: "https://your-gateway.akeyless.io" # Optional: defaults to https://api.akeyless.io - - name: accessId - value: "p-123456780wm" # Required: your Akeyless access ID - - name: accessKey - value: "ABCD1233...=" # Optional: your Akeyless access key for API key authentication - - name: jwt - value: "eyJ..." # Required: your Akeyless JWT for JWT authentication - - name: k8sAuthConfigName - value: "k8s-auth-config" - - name: k8sGatewayUrl - value: "https://gw.akeyless.svc.cluster.local" - - name: k8sServiceAccountToken - value: "eyJ..." \ No newline at end of file From de529649886cc6afad25da58da5ff08f147d8c39 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 11:11:39 -0400 Subject: [PATCH 23/28] generified dynamic/rotated secret values to return json string Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 43 +++++++----------- secretstores/akeyless/akeyless_test.go | 60 ++++++++++++-------------- secretstores/akeyless/utils.go | 26 ----------- 3 files changed, 43 insertions(+), 86 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 692b2b1f8a..c9af6f30c5 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -342,29 +342,29 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType break } - // assert type of secretRespMap to DynamicSecretResponse - var dynamicSecretResp DynamicSecretResponse + // Parse response to extract value and check for errors + var dynamicSecretResp struct { + Value string `json:"value"` + Error string `json:"error"` + } jsonBytes, marshalErr := json.Marshal(secretRespMap) if marshalErr != nil { err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) break } - if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &dynamicSecretResp); unmarshalErr != nil { - err = fmt.Errorf("failed to unmarshal secret response to DynamicSecretResponse: %w", unmarshalErr) + if unmarshalErr := json.Unmarshal(jsonBytes, &dynamicSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response: %w", unmarshalErr) break } - // take only relevant fields (DisplayName and SecretText) from response and marshal it to a JSON string - dynamicSecretResp.Secret.AppID = "" - dynamicSecretResp.Secret.EndDateTime = "" - dynamicSecretResp.Secret.KeyID = "" - dynamicSecretResp.Secret.TenantID = "" - jsonBytes, marshalErr = json.Marshal(dynamicSecretResp.Secret) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + // Check if the response contains an error + if dynamicSecretResp.Error != "" { + err = fmt.Errorf("dynamic secret retrieval error: %s", dynamicSecretResp.Error) break } - secretValue = string(jsonBytes) + + // Return the value field directly (already a JSON string with credentials) + secretValue = dynamicSecretResp.Value case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) @@ -375,23 +375,10 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType break } - // assert type of secretRespMap to RotatedSecretResponse - var rotatedSecretResp RotatedSecretResponse + // Marshal the entire response value object jsonBytes, marshalErr := json.Marshal(secretRespMap) if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) - break - } - if unmarshalErr := json.Unmarshal([]byte(jsonBytes), &rotatedSecretResp); unmarshalErr != nil { - err = fmt.Errorf("failed to unmarshal secret response to RotatedSecretResponse: %w", unmarshalErr) - break - } - - // take only relevant fields (Username and Password) from response and marshal it to a JSON string - rotatedSecretResp.Value.ApplicationID = "" - jsonBytes, marshalErr = json.Marshal(rotatedSecretResp.Value) - if marshalErr != nil { - err = fmt.Errorf("failed to marshal secret response to JSON: %w", marshalErr) + err = fmt.Errorf("failed to marshal rotated secret response to JSON: %w", marshalErr) break } secretValue = string(jsonBytes) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index ab19b99f74..782abd6048 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -75,18 +75,9 @@ var ( }, }, } - mockGetSingleDynamicSecretValueResponse = DynamicSecretResponse{ - ID: "{\"secret_name\": \"tmp.p-1234567890.GV7LR\",\"secret_key_id\": \"1234567890\"}", - Msg: "User has been added successfully to the following Group(s): [] Role(s): [] Expires on Thu Sep 25 15:54:06 UTC 2025", - Secret: DynamicSecretSecret{ - AppID: "1234567890", - DisplayName: "tmp.p-1234567890.GV7LR", - EndDateTime: "2025-09-26T14:54:05.1643791Z", - KeyID: "1234567890", - SecretText: testSecretValue, - TenantID: "1234567890", - }, - TTLInMinutes: "60", + mockGetSingleDynamicSecretValueResponse = map[string]interface{}{ + "value": "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", + "error": "", } mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless%s", mockRotatedSecretItemName) mockDescribeRotatedSecretType = AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE @@ -100,11 +91,11 @@ var ( }, }, } - mockGetSingleRotatedSecretValueResponse = RotatedSecretResponse{ - Value: RotatedSecretValue{ - Username: "abcdefghijklmnopqrstuvwxyz", - Password: testSecretValue, - ApplicationID: "1234567890", + mockGetSingleRotatedSecretValueResponse = map[string]interface{}{ + "value": map[string]interface{}{ + "username": "abcdefghijklmnopqrstuvwxyz", + "password": testSecretValue, + "application_id": "1234567890", }, } ) @@ -706,7 +697,7 @@ func TestGetSingleDynamicSecret(t *testing.T) { secretValue, err := store.GetSingleSecretValue(mockDescribeDynamicSecretName, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE) assert.NoError(t, err) - assert.Equal(t, "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}", secretValue) + assert.Equal(t, "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", secretValue) mockGateway.Close() } @@ -758,7 +749,7 @@ func TestGetSingleRotatedSecret(t *testing.T) { secretValue, err := store.GetSingleSecretValue(mockDescribeRotatedSecretName, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE) assert.NoError(t, err) - assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", secretValue) + assert.Equal(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", secretValue) mockGateway.Close() } @@ -859,13 +850,13 @@ func TestGetBulkSecretValues(t *testing.T) { // Check dynamic secret dynamicSecretKey := "/path/to/akeyless/dynamic-secret-test" assert.Contains(t, response.Data, dynamicSecretKey) - expectedDynamicValue := "{\"displayName\":\"tmp.p-1234567890.GV7LR\",\"secretText\":\"r3vE4L3D\"}" + expectedDynamicValue := "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}" assert.Equal(t, expectedDynamicValue, response.Data[dynamicSecretKey][dynamicSecretKey]) // Check rotated secret rotatedSecretKey := "/path/to/akeyless/rotated-secret-test" assert.Contains(t, response.Data, rotatedSecretKey) - assert.Equal(t, "{\"username\":\"abcdefghijklmnopqrstuvwxyz\",\"password\":\"r3vE4L3D\"}", response.Data[rotatedSecretKey][rotatedSecretKey]) + assert.Equal(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", response.Data[rotatedSecretKey][rotatedSecretKey]) mockGateway.Close() } @@ -1029,18 +1020,23 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { case "/get-dynamic-secret-value": // Create dynamic secret responses for each secret - var dynamicSecretResponse DynamicSecretResponse - dynamicSecretResponse.Secret.SecretText = "dynamic-secret-1-value" - dynamicSecretResponse.Secret.DisplayName = "dynamic-secret-1" + dynamicSecretResponse := map[string]interface{}{ + "value": "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}", + "error": "", + } jsonResponse, _ := json.Marshal(&dynamicSecretResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) case "/get-rotated-secret-value": // Create rotated secret response - var rotatedSecretResponse RotatedSecretResponse - rotatedSecretResponse.Value.Username = "rotated-user" - rotatedSecretResponse.Value.Password = "rotated-secret-1-value" + rotatedSecretResponse := map[string]interface{}{ + "value": map[string]interface{}{ + "username": "rotated-user", + "password": "rotated-secret-1-value", + "application_id": "1234567890", + }, + } jsonResponse, _ := json.Marshal(&rotatedSecretResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -1144,25 +1140,25 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { // Check dynamic secrets from /path/to/dynamic/secrets assert.Contains(t, response.Data, dynamicSecret1) - expectedDynamicValue1 := "{\"displayName\":\"dynamic-secret-1\",\"secretText\":\"dynamic-secret-1-value\"}" + expectedDynamicValue1 := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" assert.Equal(t, expectedDynamicValue1, response.Data[dynamicSecret1][dynamicSecret1]) assert.Contains(t, response.Data, dynamicSecret2) - expectedDynamicValue2 := "{\"displayName\":\"dynamic-secret-1\",\"secretText\":\"dynamic-secret-1-value\"}" + expectedDynamicValue2 := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" assert.Equal(t, expectedDynamicValue2, response.Data[dynamicSecret2][dynamicSecret2]) // Check rotated secret from /path/to/rotated/secrets assert.Contains(t, response.Data, rotatedSecret1) - expectedRotatedValue1 := "{\"username\":\"rotated-user\",\"password\":\"rotated-secret-1-value\"}" + expectedRotatedValue1 := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" assert.Equal(t, expectedRotatedValue1, response.Data[rotatedSecret1][rotatedSecret1]) // Check mixed secrets from /path/to/mixed/secrets assert.Contains(t, response.Data, mixedStaticSecret) assert.Equal(t, "mixed-static-secret-value", response.Data[mixedStaticSecret][mixedStaticSecret]) assert.Contains(t, response.Data, mixedDynamicSecret) - expectedMixedDynamicValue := "{\"displayName\":\"dynamic-secret-1\",\"secretText\":\"dynamic-secret-1-value\"}" + expectedMixedDynamicValue := "{\"user\":\"dynamic-secret-1\",\"password\":\"dynamic-secret-1-value\",\"ttl_in_minutes\":\"60\",\"id\":\"dynamic-secret-1\"}" assert.Equal(t, expectedMixedDynamicValue, response.Data[mixedDynamicSecret][mixedDynamicSecret]) assert.Contains(t, response.Data, mixedRotatedSecret) - expectedMixedRotatedValue := "{\"username\":\"rotated-user\",\"password\":\"rotated-secret-1-value\"}" + expectedMixedRotatedValue := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" assert.Equal(t, expectedMixedRotatedValue, response.Data[mixedRotatedSecret][mixedRotatedSecret]) mockGateway.Close() diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 4b1f77f60b..48e3f9a757 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -73,32 +73,6 @@ func GetAccessTypeDisplayName(typeChar string) (string, error) { return displayName, nil } -type DynamicSecretResponse struct { - ID string `json:"id"` - Msg string `json:"msg"` - Secret DynamicSecretSecret `json:"secret"` - TTLInMinutes string `json:"ttl_in_minutes"` -} - -type DynamicSecretSecret struct { - AppID string `json:"appId,omitempty"` - DisplayName string `json:"displayName"` - EndDateTime string `json:"endDateTime,omitempty"` - KeyID string `json:"keyId,omitempty"` - SecretText string `json:"secretText"` - TenantID string `json:"tenantId,omitempty"` -} - -type RotatedSecretResponse struct { - Value RotatedSecretValue `json:"value"` -} - -type RotatedSecretValue struct { - Username string `json:"username"` - Password string `json:"password"` - ApplicationID string `json:"application_id,omitempty"` -} - func GetDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { return secretstores.GetSecretResponse{ Data: map[string]string{ From 17ae7dbc8565acb8c235acae3b67a81698b4b64c Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 11:32:43 -0400 Subject: [PATCH 24/28] updated docs to include response changes Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index d7e2097a59..e4a97aa628 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -121,3 +121,51 @@ curl http://localhost:3500/v1.0/secrets/akeyless/bulk - Supports static, dynamic and rotated secrets. - **GetSecret**: Retrieve an individual value secret by path. - **BulkGetSecret**: Retrieve an all secrets from the root path. + +## Response Formats + +The Akeyless secret store returns different response formats depending on the secret type: + +### Static Secrets +Static secrets return their value directly as a string: + +```json +{ + "my-static-secret": "secret-value" +} +``` + +### Dynamic Secrets +Dynamic secrets return a JSON string containing the credentials. The exact structure depends on the target system: + +**MySQL Dynamic Secret:** +```json +{ + "my-mysql-secret": "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}" +} +``` + +**Azure AD Dynamic Secret:** +```json +{ + "my-azure-secret": "{\"user\":{\"id\":\"user_id\",\"displayName\":\"user_name\",\"mail\":\"email@domain.com\"},\"secret\":{\"keyId\":\"secret_key_id\",\"displayName\":\"secret_name\",\"tenantId\":\"tenant_id\"},\"ttl_in_minutes\":\"60\",\"id\":\"user_id\",\"msg\":\"User has been added successfully...\"}" +} +``` + +**GCP Dynamic Secret:** +```json +{ + "my-gcp-secret": "{\"encoded_key\":\"base64_encoded_service_account_key\",\"ttl_in_minutes\":\"60\",\"id\":\"service_account_name\"}" +} +``` + +### Rotated Secrets +Rotated secrets return a JSON object containing all available fields: + +```json +{ + "my-rotated-secret": "{\"value\":{\"username\":\"rotated_user\",\"password\":\"rotated_password\",\"application_id\":\"1234567890\"}}" +} +``` + +**Note:** The exact fields in dynamic and rotated secret responses vary by target system and configuration. Applications should parse the JSON string to extract the specific credentials they need. From a31c98fb12c6fc351859e48074997e9412999eb3 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 11:59:42 -0400 Subject: [PATCH 25/28] shortened consts Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 40 +++++++++++++------------- secretstores/akeyless/akeyless_test.go | 30 +++++++++---------- secretstores/akeyless/utils.go | 37 ++++++++++++------------ 3 files changed, 53 insertions(+), 54 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index c9af6f30c5..eaa3af7bfa 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -150,7 +150,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore defer wg.Done() if len(staticItemNames) == 1 { staticSecretName := staticItemNames[0] - value, err := a.GetSingleSecretValue(staticSecretName, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE) + value, err := a.GetSingleSecretValue(staticSecretName, STATIC_SECRET_RESPONSE) secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} } else { secretResponse := a.GetBulkStaticSecretValues(staticItemNames) @@ -167,7 +167,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range dynamicItemNames { - value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE) + value, err := a.GetSingleSecretValue(item, DYNAMIC_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -181,7 +181,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range rotatedItemNames { - value, err := a.GetSingleSecretValue(item, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE) + value, err := a.GetSingleSecretValue(item, ROTATED_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -266,11 +266,11 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle a.logger.Debugf("access type detected: %s", accessTypeDisplayName) switch accessTypeDisplayName { - case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + case DEFAULT_AUTH_TYPE: if m.AccessKey == "" { return nil, errors.New("accessKey is required") } - case AKEYLESS_AUTH_ACCESS_JWT: + case AUTH_JWT: if m.JWT == "" { return nil, errors.New("jwt is required") } @@ -279,8 +279,8 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle // Set default gateway URL if not specified if m.GatewayURL == "" { - a.logger.Infof("Gateway URL is not set, using default value %s...", AKEYLESS_PUBLIC_GATEWAY_URL) - m.GatewayURL = AKEYLESS_PUBLIC_GATEWAY_URL + a.logger.Infof("Gateway URL is not set, using default value %s...", PUBLIC_GATEWAY_URL) + m.GatewayURL = PUBLIC_GATEWAY_URL } return &m, nil @@ -309,7 +309,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType var err error switch secretType { - case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + case STATIC_SECRET_RESPONSE: getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) getSecretValue.SetToken(a.token) secretRespMap, _, apiErr := a.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() @@ -333,7 +333,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType break } - case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + case DYNAMIC_SECRET_RESPONSE: getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) getDynamicSecretValue.SetToken(a.token) secretRespMap, _, apiErr := a.v2.GetDynamicSecretValue(context.Background()).Body(*getDynamicSecretValue).Execute() @@ -366,7 +366,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType // Return the value field directly (already a JSON string with credentials) secretValue = dynamicSecretResp.Value - case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + case ROTATED_SECRET_RESPONSE: getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) getRotatedSecretValue.SetToken(a.token) secretRespMap, _, apiErr := a.v2.GetRotatedSecretValue(context.Background()).Body(*getRotatedSecretValue).Execute() @@ -418,7 +418,7 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item listItems.SetToken(a.token) listItems.SetPath(path) listItems.SetAutoPagination("enabled") - listItems.SetType([]string{AKEYLESS_SECRET_TYPE_STATIC, AKEYLESS_SECRET_TYPE_DYNAMIC, AKEYLESS_SECRET_TYPE_ROTATED}) + listItems.SetType(SUPPORTED_SECRET_TYPES) // Execute the list items request a.logger.Debugf("listing items from path '%s'...", path) @@ -462,18 +462,18 @@ func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { // Depending on the access type we set the appropriate authentication method switch accessType { // If access type is AWS IAM we use the cloud ID - case AKEYLESS_AUTH_ACCESS_IAM: + case AUTH_IAM: id, err := aws.GetCloudId() if err != nil { return errors.New("unable to get cloud ID") } authRequest.SetCloudId(id) - case AKEYLESS_AUTH_ACCESS_JWT: + case AUTH_JWT: authRequest.SetJwt(metadata.JWT) - case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + case DEFAULT_AUTH_TYPE: a.logger.Debug("authenticating using access key...") authRequest.SetAccessKey(metadata.AccessKey) - case AKEYLESS_AUTH_ACCESS_K8S: + case AUTH_K8S: a.logger.Debug("authenticating using k8s...") err := setK8SAuthConfiguration(*metadata, authRequest, a) if err != nil { @@ -489,8 +489,8 @@ func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { URL: metadata.GatewayURL, }, } - config.UserAgent = AKEYLESS_USER_AGENT - config.AddDefaultHeader("akeylessclienttype", AKEYLESS_USER_AGENT) + config.UserAgent = USER_AGENT + config.AddDefaultHeader("akeylessclienttype", USER_AGENT) a.v2 = akeyless.NewAPIClient(config).V2Api @@ -514,11 +514,11 @@ func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akey itemType := *item.ItemType switch itemType { - case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + case STATIC_SECRET_RESPONSE: staticItems = append(staticItems, item) - case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + case DYNAMIC_SECRET_RESPONSE: dynamicItems = append(dynamicItems, item) - case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + case ROTATED_SECRET_RESPONSE: rotatedItems = append(rotatedItems, item) } } diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 782abd6048..bd79eea813 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -39,7 +39,7 @@ var ( mockDynamicSecretItemName = "/dynamic-secret-test" mockRotatedSecretItemName = "/rotated-secret-test" mockDescribeStaticSecretName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretItem) - mockDescribeStaticSecretType = AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE + mockDescribeStaticSecretType = STATIC_SECRET_RESPONSE mockDescribeStaticSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeStaticSecretName, ItemType: &mockDescribeStaticSecretType, @@ -64,7 +64,7 @@ var ( }, } mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) - mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE + mockDescribeDynamicSecretType = DYNAMIC_SECRET_RESPONSE mockDescribeDynamicSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeDynamicSecretName, ItemType: &mockDescribeDynamicSecretType, @@ -80,7 +80,7 @@ var ( "error": "", } mockDescribeRotatedSecretName = fmt.Sprintf("/path/to/akeyless%s", mockRotatedSecretItemName) - mockDescribeRotatedSecretType = AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE + mockDescribeRotatedSecretType = ROTATED_SECRET_RESPONSE mockDescribeRotatedSecretItemResponse = akeyless.Item{ ItemName: &mockDescribeRotatedSecretName, ItemType: &mockDescribeRotatedSecretType, @@ -119,13 +119,13 @@ func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessS // Depending on the access type we set the appropriate authentication method switch metadata.AccessType { // If access type is AWS IAM we use the mock cloud ID - case AKEYLESS_AUTH_ACCESS_IAM: + case AUTH_IAM: akeylessSecretStore.logger.Debug("Using mock cloud ID for AWS IAM...") authRequest.SetCloudId(mockCloudID) - case AKEYLESS_AUTH_ACCESS_JWT: + case AUTH_JWT: akeylessSecretStore.logger.Debug("Setting JWT for authentication...") authRequest.SetJwt(metadata.JWT) - case AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE: + case DEFAULT_AUTH_TYPE: akeylessSecretStore.logger.Debug("Setting access key for authentication...") authRequest.SetAccessKey(metadata.AccessKey) } @@ -136,8 +136,8 @@ func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessS URL: metadata.GatewayURL, }, } - config.UserAgent = AKEYLESS_USER_AGENT - config.AddDefaultHeader("akeylessclienttype", AKEYLESS_USER_AGENT) + config.UserAgent = USER_AGENT + config.AddDefaultHeader("akeylessclienttype", USER_AGENT) akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api @@ -319,7 +319,7 @@ func TestParseMetadata(t *testing.T) { expected: &akeylessMetadata{ AccessID: testAccessIdKey, AccessKey: testAccessKey, - AccessType: AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE, + AccessType: DEFAULT_AUTH_TYPE, GatewayURL: "https://api.akeyless.io", // Default gateway URL }, }, @@ -334,7 +334,7 @@ func TestParseMetadata(t *testing.T) { expected: &akeylessMetadata{ AccessID: testAccessIdJwt, JWT: testJWT, - AccessType: AKEYLESS_AUTH_ACCESS_JWT, + AccessType: AUTH_JWT, GatewayURL: mockGateway.URL, }, }, @@ -347,7 +347,7 @@ func TestParseMetadata(t *testing.T) { expectError: false, expected: &akeylessMetadata{ AccessID: testAccessIdIAM, - AccessType: AKEYLESS_AUTH_ACCESS_IAM, + AccessType: AUTH_IAM, GatewayURL: mockGateway.URL, }, }, @@ -442,7 +442,7 @@ func TestMockAWSCloudID(t *testing.T) { // Parse metadata first m, err := store.parseMetadata(meta) require.NoError(t, err) - assert.Equal(t, AKEYLESS_AUTH_ACCESS_IAM, m.AccessType) + assert.Equal(t, AUTH_IAM, m.AccessType) // Use mock authentication with mock cloud ID err = mockAuthenticate(m, store) @@ -647,7 +647,7 @@ func TestGetSecretType(t *testing.T) { secretType, err := store.GetSecretType(mockDescribeStaticSecretName) assert.NoError(t, err) - assert.Equal(t, AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE, secretType) + assert.Equal(t, STATIC_SECRET_RESPONSE, secretType) } func TestGetSingleDynamicSecret(t *testing.T) { @@ -695,7 +695,7 @@ func TestGetSingleDynamicSecret(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) - secretValue, err := store.GetSingleSecretValue(mockDescribeDynamicSecretName, AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE) + secretValue, err := store.GetSingleSecretValue(mockDescribeDynamicSecretName, DYNAMIC_SECRET_RESPONSE) assert.NoError(t, err) assert.Equal(t, "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", secretValue) @@ -747,7 +747,7 @@ func TestGetSingleRotatedSecret(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) - secretValue, err := store.GetSingleSecretValue(mockDescribeRotatedSecretName, AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE) + secretValue, err := store.GetSingleSecretValue(mockDescribeRotatedSecretName, ROTATED_SECRET_RESPONSE) assert.NoError(t, err) assert.Equal(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", secretValue) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 48e3f9a757..f0db11fe31 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -15,26 +15,25 @@ import ( // Define constants for the access types. These are equivalent to the TypeScript consts. const ( - AKEYLESS_AUTH_ACCESS_JWT = "jwt" - AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE = "access_key" - AKEYLESS_AUTH_ACCESS_IAM = "aws_iam" - AKEYLESS_AUTH_ACCESS_K8S = "k8s" - AKEYLESS_PUBLIC_GATEWAY_URL = "https://api.akeyless.io" - AKEYLESS_USER_AGENT = "dapr.io/akeyless-secret-store" - AKEYLESS_SECRET_TYPE_STATIC = "static-secret" - AKEYLESS_SECRET_TYPE_DYNAMIC = "dynamic-secret" - AKEYLESS_SECRET_TYPE_ROTATED = "rotated-secret" - AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE = "STATIC_SECRET" - AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE = "DYNAMIC_SECRET" - AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE = "ROTATED_SECRET" + AUTH_JWT = "jwt" + DEFAULT_AUTH_TYPE = "access_key" + AUTH_IAM = "aws_iam" + AUTH_K8S = "k8s" + PUBLIC_GATEWAY_URL = "https://api.akeyless.io" + USER_AGENT = "dapr.io/akeyless-secret-store" + STATIC_SECRET_RESPONSE = "STATIC_SECRET" + DYNAMIC_SECRET_RESPONSE = "DYNAMIC_SECRET" + ROTATED_SECRET_RESPONSE = "ROTATED_SECRET" ) +var SUPPORTED_SECRET_TYPES = []string{"static-secret", "dynamic-secret", "rotated-secret"} + // AccessTypeCharMap maps single-character access types to their display names. var AccessTypeCharMap = map[string]string{ - "a": AKEYLESS_AUTH_DEFAULT_ACCESS_TYPE, - "o": AKEYLESS_AUTH_ACCESS_JWT, - "w": AKEYLESS_AUTH_ACCESS_IAM, - "k": AKEYLESS_AUTH_ACCESS_K8S, + "a": DEFAULT_AUTH_TYPE, + "o": AUTH_JWT, + "w": AUTH_IAM, + "k": AUTH_K8S, } // AccessIdRegex is the compiled regular expression for validating Akeyless Access IDs. @@ -148,10 +147,10 @@ func isSecretActive(secret akeyless.Item, logger logger.Logger) bool { } switch *secret.ItemType { - case AKEYLESS_SECRET_TYPE_STATIC_SECRET_RESPONSE: + case STATIC_SECRET_RESPONSE: logger.Debugf("static secret '%s' is active", *secret.ItemName) isActive = true - case AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE: + case DYNAMIC_SECRET_RESPONSE: // Check if ItemGeneralInfo is available, if not, include the secret if secret.ItemGeneralInfo != nil && secret.ItemGeneralInfo.DynamicSecretProducerDetails != nil && @@ -168,7 +167,7 @@ func isSecretActive(secret akeyless.Item, logger logger.Logger) bool { logger.Debugf("dynamic secret '%s' is missing detailed info. adding to filtered secrets...", *secret.ItemName) isActive = true } - case AKEYLESS_SECRET_TYPE_ROTATED_SECRET_RESPONSE: + case ROTATED_SECRET_RESPONSE: // Check if ItemGeneralInfo is available, if not, include the secret if secret.ItemGeneralInfo != nil && secret.ItemGeneralInfo.RotatedSecretDetails != nil && From 5d2833206b183a9a513253b93b06498b36fa9afb Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 12:21:54 -0400 Subject: [PATCH 26/28] propagated ctx Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 42 +++++++++++++------------- secretstores/akeyless/akeyless_test.go | 19 ++++++------ 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index eaa3af7bfa..7d06d6b633 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -54,7 +54,7 @@ func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metada return errors.New("failed to parse metadata: " + err.Error()) } - err = a.Authenticate(m) + err = a.Authenticate(ctx, m) if err != nil { return errors.New("failed to authenticate with Akeyless: " + err.Error()) } @@ -69,14 +69,14 @@ func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.Ge } a.logger.Debugf("getting secret type for '%s'...", req.Name) - secretType, err := a.GetSecretType(req.Name) + secretType, err := a.GetSecretType(ctx, req.Name) if err != nil { return secretstores.GetSecretResponse{}, err } a.logger.Debugf("getting secret value for '%s' (type %s)...", req.Name, secretType) - secretValue, err := a.GetSingleSecretValue(req.Name, secretType) + secretValue, err := a.GetSingleSecretValue(ctx, req.Name, secretType) if err != nil { return secretstores.GetSecretResponse{}, errors.New(err.Error()) } @@ -104,7 +104,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore // For bulk get, we need to list all secrets first a.logger.Debug("listing items from / path...") - listItems, err := a.listItemsRecursively("/") + listItems, err := a.listItemsRecursively(ctx, "/") if err != nil { return response, fmt.Errorf("failed to list items from Akeyless: %w", err) } @@ -150,10 +150,10 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore defer wg.Done() if len(staticItemNames) == 1 { staticSecretName := staticItemNames[0] - value, err := a.GetSingleSecretValue(staticSecretName, STATIC_SECRET_RESPONSE) + value, err := a.GetSingleSecretValue(ctx, staticSecretName, STATIC_SECRET_RESPONSE) secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: err} } else { - secretResponse := a.GetBulkStaticSecretValues(staticItemNames) + secretResponse := a.GetBulkStaticSecretValues(ctx, staticItemNames) if len(secretResponse) > 0 { for _, result := range secretResponse { secretResultChannels <- result @@ -167,7 +167,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range dynamicItemNames { - value, err := a.GetSingleSecretValue(item, DYNAMIC_SECRET_RESPONSE) + value, err := a.GetSingleSecretValue(ctx, item, DYNAMIC_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -181,7 +181,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range rotatedItemNames { - value, err := a.GetSingleSecretValue(item, ROTATED_SECRET_RESPONSE) + value, err := a.GetSingleSecretValue(ctx, item, ROTATED_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -286,10 +286,10 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle return &m, nil } -func (a *akeylessSecretStore) GetSecretType(secretName string) (string, error) { +func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName string) (string, error) { describeItem := akeyless.NewDescribeItem(secretName) describeItem.SetToken(a.token) - describeItemResp, _, err := a.v2.DescribeItem(context.Background()).Body(*describeItem).Execute() + describeItemResp, _, err := a.v2.DescribeItem(ctx).Body(*describeItem).Execute() if err != nil { return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) } @@ -303,7 +303,7 @@ func (a *akeylessSecretStore) GetSecretType(secretName string) (string, error) { // GetSingleSecretValue gets the value of a single secret from Akeyless. // It returns the value of the secret or an error if the secret is not found. -func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType string) (string, error) { +func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretName string, secretType string) (string, error) { var secretValue string var err error @@ -312,7 +312,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType case STATIC_SECRET_RESPONSE: getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) getSecretValue.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetSecretValue(context.Background()).Body(*getSecretValue).Execute() + secretRespMap, _, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() if apiErr != nil { err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) break @@ -336,7 +336,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType case DYNAMIC_SECRET_RESPONSE: getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) getDynamicSecretValue.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetDynamicSecretValue(context.Background()).Body(*getDynamicSecretValue).Execute() + secretRespMap, _, apiErr := a.v2.GetDynamicSecretValue(ctx).Body(*getDynamicSecretValue).Execute() if apiErr != nil { err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) break @@ -369,7 +369,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType case ROTATED_SECRET_RESPONSE: getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) getRotatedSecretValue.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetRotatedSecretValue(context.Background()).Body(*getRotatedSecretValue).Execute() + secretRespMap, _, apiErr := a.v2.GetRotatedSecretValue(ctx).Body(*getRotatedSecretValue).Execute() if apiErr != nil { err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) break @@ -389,13 +389,13 @@ func (a *akeylessSecretStore) GetSingleSecretValue(secretName string, secretType // GetBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. // It returns a map of secret names and their values. -func (a *akeylessSecretStore) GetBulkStaticSecretValues(secretNames []string) []secretResultCollection { +func (a *akeylessSecretStore) GetBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { var secretResponse = make([]secretResultCollection, len(secretNames)) getSecretsValues := akeyless.NewGetSecretValue(secretNames) getSecretsValues.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetSecretValue(context.Background()).Body(*getSecretsValues).Execute() + secretRespMap, _, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() if apiErr != nil { secretResponse = append(secretResponse, secretResultCollection{name: "", value: "", err: fmt.Errorf("failed to get static secrets' '%s' value from Akeyless API: %w", secretNames, apiErr)}) } else { @@ -410,7 +410,7 @@ func (a *akeylessSecretStore) GetBulkStaticSecretValues(secretNames []string) [] // listItemsRecursively lists all items in a given path recursively. // It returns a list of items and an error if the list items request fails. -func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item, error) { +func (a *akeylessSecretStore) listItemsRecursively(ctx context.Context, path string) ([]akeyless.Item, error) { var allItems []akeyless.Item // Create the list items request @@ -422,7 +422,7 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item // Execute the list items request a.logger.Debugf("listing items from path '%s'...", path) - itemsList, _, err := a.v2.ListItems(context.Background()).Body(*listItems).Execute() + itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() if err != nil { return nil, err } @@ -435,7 +435,7 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item // Recursively process each subfolder if itemsList.Folders != nil { for _, folder := range itemsList.Folders { - subItems, err := a.listItemsRecursively(folder) + subItems, err := a.listItemsRecursively(ctx, folder) if err != nil { return nil, err } @@ -448,7 +448,7 @@ func (a *akeylessSecretStore) listItemsRecursively(path string) ([]akeyless.Item // Authenticate authenticates with Akeyless using the provided metadata. // It returns an error if the authentication fails. -func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { +func (a *akeylessSecretStore) Authenticate(ctx context.Context, metadata *akeylessMetadata) error { a.logger.Debug("Creating authentication request to Akeyless...") authRequest := akeyless.NewAuth() @@ -495,7 +495,7 @@ func (a *akeylessSecretStore) Authenticate(metadata *akeylessMetadata) error { a.v2 = akeyless.NewAPIClient(config).V2Api a.logger.Debug("authenticating with Akeyless...") - out, _, err := a.v2.Auth(context.Background()).Body(*authRequest).Execute() + out, _, err := a.v2.Auth(ctx).Body(*authRequest).Execute() if err != nil { return fmt.Errorf("failed to authenticate with Akeyless: %w", err) } diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index bd79eea813..da184505c3 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -642,10 +642,11 @@ func TestGetSecretType(t *testing.T) { }, } - err := store.Init(context.Background(), meta) + ctx := context.Background() + err := store.Init(ctx, meta) require.NoError(t, err) - secretType, err := store.GetSecretType(mockDescribeStaticSecretName) + secretType, err := store.GetSecretType(ctx, mockDescribeStaticSecretName) assert.NoError(t, err) assert.Equal(t, STATIC_SECRET_RESPONSE, secretType) } @@ -692,18 +693,15 @@ func TestGetSingleDynamicSecret(t *testing.T) { }, } - err := store.Init(context.Background(), meta) + ctx := context.Background() + err := store.Init(ctx, meta) require.NoError(t, err) - - secretValue, err := store.GetSingleSecretValue(mockDescribeDynamicSecretName, DYNAMIC_SECRET_RESPONSE) + secretValue, err := store.GetSingleSecretValue(ctx, mockDescribeDynamicSecretName, DYNAMIC_SECRET_RESPONSE) assert.NoError(t, err) assert.Equal(t, "{\"user\":\"generated_username\",\"password\":\"generated_password\",\"ttl_in_minutes\":\"60\",\"id\":\"username\"}", secretValue) - mockGateway.Close() } - func TestGetSingleRotatedSecret(t *testing.T) { - var mockGateway *httptest.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -744,10 +742,11 @@ func TestGetSingleRotatedSecret(t *testing.T) { }, } - err := store.Init(context.Background(), meta) + ctx := context.Background() + err := store.Init(ctx, meta) require.NoError(t, err) - secretValue, err := store.GetSingleSecretValue(mockDescribeRotatedSecretName, ROTATED_SECRET_RESPONSE) + secretValue, err := store.GetSingleSecretValue(ctx, mockDescribeRotatedSecretName, ROTATED_SECRET_RESPONSE) assert.NoError(t, err) assert.Equal(t, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", secretValue) From 8a0522bf58ad15b0f4fc79a7304244f82b7efd45 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 12:27:27 -0400 Subject: [PATCH 27/28] validate 200 returned for auth request Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 7d06d6b633..dbd85a369d 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -495,9 +495,9 @@ func (a *akeylessSecretStore) Authenticate(ctx context.Context, metadata *akeyle a.v2 = akeyless.NewAPIClient(config).V2Api a.logger.Debug("authenticating with Akeyless...") - out, _, err := a.v2.Auth(ctx).Body(*authRequest).Execute() - if err != nil { - return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + out, httpResponse, err := a.v2.Auth(ctx).Body(*authRequest).Execute() + if err != nil || httpResponse.StatusCode != 200 { + return fmt.Errorf("failed to authenticate with Akeyless: %w", errors.New(httpResponse.Status)) } a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) From b2a72bf91ca2371c2dd3d0845167cfe48c1827ba Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 22 Oct 2025 12:40:30 -0400 Subject: [PATCH 28/28] mv auth func up Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 120 +++++++++++++++--------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index dbd85a369d..642d08922d 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -62,6 +62,66 @@ func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metada return nil } +// Authenticate authenticates with Akeyless using the provided metadata. +// It returns an error if the authentication fails. +func (a *akeylessSecretStore) Authenticate(ctx context.Context, metadata *akeylessMetadata) error { + + a.logger.Debug("Creating authentication request to Akeyless...") + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + authRequest.SetAccessType(metadata.AccessType) + + var accessType = metadata.AccessType + + a.logger.Debugf("authenticating using access type: %s", accessType) + + // Depending on the access type we set the appropriate authentication method + switch accessType { + // If access type is AWS IAM we use the cloud ID + case AUTH_IAM: + id, err := aws.GetCloudId() + if err != nil { + return errors.New("unable to get cloud ID") + } + authRequest.SetCloudId(id) + case AUTH_JWT: + authRequest.SetJwt(metadata.JWT) + case DEFAULT_AUTH_TYPE: + a.logger.Debug("authenticating using access key...") + authRequest.SetAccessKey(metadata.AccessKey) + case AUTH_K8S: + a.logger.Debug("authenticating using k8s...") + err := setK8SAuthConfiguration(*metadata, authRequest, a) + if err != nil { + return fmt.Errorf("failed to set k8s auth configuration: %w", err) + } + } + + // Create Akeyless API client configuration + a.logger.Debug("creating Akeyless API client configuration...") + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = USER_AGENT + config.AddDefaultHeader("akeylessclienttype", USER_AGENT) + + a.v2 = akeyless.NewAPIClient(config).V2Api + + a.logger.Debug("authenticating with Akeyless...") + out, httpResponse, err := a.v2.Auth(ctx).Body(*authRequest).Execute() + if err != nil || httpResponse.StatusCode != 200 { + return fmt.Errorf("failed to authenticate with Akeyless: %w", errors.New(httpResponse.Status)) + } + + a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) + a.token = out.GetToken() + + return nil +} + // GetSecret retrieves a secret using a key and returns a map of decrypted string/string values. func (a *akeylessSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) { if a.v2 == nil { @@ -446,66 +506,6 @@ func (a *akeylessSecretStore) listItemsRecursively(ctx context.Context, path str return allItems, nil } -// Authenticate authenticates with Akeyless using the provided metadata. -// It returns an error if the authentication fails. -func (a *akeylessSecretStore) Authenticate(ctx context.Context, metadata *akeylessMetadata) error { - - a.logger.Debug("Creating authentication request to Akeyless...") - authRequest := akeyless.NewAuth() - authRequest.SetAccessId(metadata.AccessID) - authRequest.SetAccessType(metadata.AccessType) - - var accessType = metadata.AccessType - - a.logger.Debugf("authenticating using access type: %s", accessType) - - // Depending on the access type we set the appropriate authentication method - switch accessType { - // If access type is AWS IAM we use the cloud ID - case AUTH_IAM: - id, err := aws.GetCloudId() - if err != nil { - return errors.New("unable to get cloud ID") - } - authRequest.SetCloudId(id) - case AUTH_JWT: - authRequest.SetJwt(metadata.JWT) - case DEFAULT_AUTH_TYPE: - a.logger.Debug("authenticating using access key...") - authRequest.SetAccessKey(metadata.AccessKey) - case AUTH_K8S: - a.logger.Debug("authenticating using k8s...") - err := setK8SAuthConfiguration(*metadata, authRequest, a) - if err != nil { - return fmt.Errorf("failed to set k8s auth configuration: %w", err) - } - } - - // Create Akeyless API client configuration - a.logger.Debug("creating Akeyless API client configuration...") - config := akeyless.NewConfiguration() - config.Servers = []akeyless.ServerConfiguration{ - { - URL: metadata.GatewayURL, - }, - } - config.UserAgent = USER_AGENT - config.AddDefaultHeader("akeylessclienttype", USER_AGENT) - - a.v2 = akeyless.NewAPIClient(config).V2Api - - a.logger.Debug("authenticating with Akeyless...") - out, httpResponse, err := a.v2.Auth(ctx).Body(*authRequest).Execute() - if err != nil || httpResponse.StatusCode != 200 { - return fmt.Errorf("failed to authenticate with Akeyless: %w", errors.New(httpResponse.Status)) - } - - a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) - a.token = out.GetToken() - - return nil -} - func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]akeyless.Item, []akeyless.Item, []akeyless.Item) { staticItems := []akeyless.Item{} dynamicItems := []akeyless.Item{}