From fc7b3d13827a4b6c24fcf77abe7a45705546e48f Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Mon, 1 Dec 2025 13:37:56 -0500 Subject: [PATCH 01/19] Add Akeyless Secrets Store Component Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 213 +++ secretstores/akeyless/akeyless.go | 569 +++++++ secretstores/akeyless/akeyless_test.go | 1883 ++++++++++++++++++++++++ secretstores/akeyless/metadata.yaml | 78 + secretstores/akeyless/utils.go | 266 ++++ 5 files changed, 3009 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/metadata.yaml create mode 100644 secretstores/akeyless/utils.go diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md new file mode 100644 index 0000000000..da736115b6 --- /dev/null +++ b/secretstores/akeyless/README.md @@ -0,0 +1,213 @@ +# 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 Dapr Secret Store component only supports 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) +- [Kubernetes](https://docs.akeyless.io/docs/kubernetes-auth) + +### Authentication + +The Akeyless secret store component supports the following configuration options: + +| Field | Required | Description | Example | +|-------|----------|-------------|---------| +| `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...=` | +| `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...` | + + + +## Examples + +We currently support the following [Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods): + + +## Examples + +## Example Configuration: API Key + +```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-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 +spec: + type: secretstores.akeyless + version: v1 + metadata: + - name: gatewayUrl + value: "https://your-gateway.akeyless.io" + - name: accessId + 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: + +```bash +# Get a single secret +curl http://localhost:3500/v1.0/secrets/akeyless/my-secret + +# Get all secrets (static, dynamic, rotated) from root (/) path +curl http://localhost:3500/v1.0/secrets/akeyless/bulk + +# Get all secrets static secrets +curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=static + +# Get all static and dynamic secrets from a specific path (/my/org) +curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=static,dynamic&metadata.path=/my/org +``` + +Or using the Dapr SDK. The example below retrieves all static secrets from path `/path/to/department`. +```go +log.Println("Starting test application") + client, err := dapr.NewClient() + if err != nil { + log.Printf("Error creating Dapr client: %v\n", err) + panic(err) + } + log.Println("Dapr client created successfully") + const daprSecretStore = "akeyless" + + defer client.Close() + ctx := context.Background() + akeylessBulkMetadata := map[string]string{ + "path": "/path/to/department", + "secrets_type": "static", + } + secrets, err := client.GetBulkSecret(ctx, daprSecretStore, akeylessBulkMetadata) + if err != nil { + log.Printf("Error fetching secrets: %v\n", err) + panic(err) + } + log.Printf("Found %d secrets: ", len(secrets)) + for secretName, secretValue := range secrets { + log.Printf("Secret: %s, Value: %s", secretName, secretValue) + } +``` + +## Features + +- Supports static, dynamic and rotated secrets. +- **GetSecret**: Retrieve an individual value secret by path. +- **BulkGetSecret**: Retrieve all secrets from a specified path (or `/` by default) recursively. + +## 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. diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go new file mode 100644 index 0000000000..617ad367e2 --- /dev/null +++ b/secretstores/akeyless/akeyless.go @@ -0,0 +1,569 @@ +package akeyless + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + "sync" + + aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + "github.com/akeylesslabs/akeyless-go/v5" + + "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 { + v2 *akeyless.V2ApiService + 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"` + JWT string `json:"jwt" mapstructure:"jwt"` + AccessID string `json:"accessId" mapstructure:"accessId"` + AccessKey string `json:"accessKey" mapstructure:"accessKey"` + 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 +// 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...") + m, err := a.parseMetadata(meta) + if err != nil { + return errors.New("failed to parse metadata: " + err.Error()) + } + + err = a.authenticate(ctx, m) + if err != nil { + return errors.New("failed to authenticate with Akeyless: " + err.Error()) + } + + 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) + + // Get the authentication method + a.logger.Debug("extracting access type from accessId...") + accessTypeChar, err := extractAccessTypeChar(metadata.AccessID) + if err != nil { + return 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.Debugf("getting access type display name for character '%s'...", accessTypeChar) + accessType, err := getAccessTypeDisplayName(accessTypeChar) + if err != nil { + return 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.Debugf("authenticating using access type '%s'", accessType) + + // Depending on the access type we set the appropriate authentication method + switch accessType { + case DEFAULT_AUTH_TYPE: + if metadata.AccessKey == "" { + return errors.New("accessKey is required for API key authentication") + } + authRequest.SetAccessKey(metadata.AccessKey) + case AUTH_IAM: + id, err := aws.GetCloudId() + if err != nil { + return errors.New("unable to get cloud ID: " + err.Error()) + } + authRequest.SetCloudId(id) + case AUTH_JWT: + if metadata.JWT == "" { + return errors.New("jwt is required for JWT authentication") + } + authRequest.SetJwt(metadata.JWT) + case AUTH_K8S: + err := setK8SAuthConfiguration(*metadata, authRequest, a) + if err != nil { + return errors.New("failed to set k8s auth configuration: " + err.Error()) + } + } + + // 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(CLIENT_SOURCE, 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 (HTTP status code: %d): %w", httpResponse.StatusCode, 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 { + return secretstores.GetSecretResponse{}, errors.New("akeyless client not initialized") + } + + a.logger.Debugf("getting secret type for '%s'...", req.Name) + secretType, err := a.GetSecretType(ctx, req.Name) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New("failed to get secret type: " + err.Error()) + } + + a.logger.Debugf("getting secret value for '%s' (type %s)...", req.Name, secretType) + + secretValue, err := a.GetSingleSecretValue(ctx, req.Name, secretType) + if err != nil { + return secretstores.GetSecretResponse{}, errors.New(err.Error()) + } + a.logger.Debugf("successfully retrieved secret '%s'", req.Name) + + return getDaprSingleSecretResponse(req.Name, secretValue) +} + +// 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. 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") + } + + // initialize response + response := secretstores.BulkGetSecretResponse{ + Data: make(map[string]map[string]string), + } + + // get secrets path to retrieve secrets from + // use root path if not specified + var secretsPath string + if value, ok := req.Metadata[METADATA_PATH_KEY]; ok { + + // normalize path + if !strings.HasPrefix(value, "/") { + secretsPath = "/" + value + } + + a.logger.Debugf("using path '%s' from metadata...", secretsPath) + } else { + a.logger.Debugf("no path found in metadata, using default path '%s'", PATH_DEFAULT) + secretsPath = PATH_DEFAULT + } + + // get secrets type to retrieve secrets from + // use all types if not specified + var requestedTypes []string + if value, ok := req.Metadata[METADATA_SECRETS_TYPE_KEY]; ok { + parsedTypes, err := parseSecretTypes(value) + if err != nil { + return response, fmt.Errorf("invalid secrets_type metadata: %w", err) + } + requestedTypes = parsedTypes + a.logger.Debugf("using secrets types '%v' from metadata...", requestedTypes) + } else { + a.logger.Debugf("no '%s' found in metadata, using all supported secret types '%v'", METADATA_SECRETS_TYPE_KEY, supportedSecretTypes) + requestedTypes = supportedSecretTypes + } + + // For bulk get, we need to list all secrets first + a.logger.Debugf("listing items from '%s' path with types '%v'...", secretsPath, requestedTypes) + listItems, err := a.listItemsRecursively(ctx, secretsPath, requestedTypes) + if err != nil { + 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 + } + + // filter out inactive secrets + a.logger.Debugf("%d items before filtering out inactive secrets", len(listItems)) + listItems = a.filterInactiveSecrets(listItems) + a.logger.Debugf("%d items remaining after filtering out inactive secrets", len(listItems)) + + // separate items by type since only static secrets are supported for bulk get + staticItemNames, dynamicItemNames, rotatedItemNames := a.separateItemsByType(listItems) + a.logger.Infof("%d items returned (static: %d, dynamic: %d, rotated: %d)", len(listItems), len(staticItemNames), len(dynamicItemNames), len(rotatedItemNames)) + + haveStaticItems := len(staticItemNames) > 0 + haveDynamicItems := len(dynamicItemNames) > 0 + haveRotatedItems := len(rotatedItemNames) > 0 + + secretResultChannels := make(chan secretResultCollection, len(listItems)) + + // 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(ctx, staticSecretName, STATIC_SECRET_RESPONSE) + if err != nil { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: "", err: err} + } else { + secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: nil} + } + } else { + secretResponse := a.GetBulkStaticSecretValues(ctx, 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(ctx, item, DYNAMIC_SECRET_RESPONSE) + 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(ctx, item, ROTATED_SECRET_RESPONSE) + 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.Errorf("error getting secret '%s': %s. Skipping...", result.name, result.err.Error()) + continue + } + + response.Data[result.name] = map[string]string{result.name: result.value} + } + + // Use the new BulkGetSecretResponse function to handle all secret types properly + // return BulkGetSecretResponse(ctx, itemsList.Items, a) + return response, nil +} + +// Features returns the features available in this secret store. +func (a *akeylessSecretStore) Features() []secretstores.Feature { + return []secretstores.Feature{} +} + +// 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) { + + a.logger.Debug("Parsing metadata...") + var m akeylessMetadata + err := kitmd.DecodeMetadata(meta.Properties, &m) + if err != nil { + return nil, err + } + + // 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})") + } + + // Set default gateway URL if not specified + if m.GatewayURL == "" { + a.logger.Infof("Gateway URL is not set, using default value %s...", PUBLIC_GATEWAY_URL) + m.GatewayURL = PUBLIC_GATEWAY_URL + } else { + _, err = url.ParseRequestURI(m.GatewayURL) + if err != nil { + return nil, fmt.Errorf("invalid gateway URL '%s': %w", m.GatewayURL, err) + } + } + + // Trim trailing slash from gateway URL + m.GatewayURL = strings.TrimSuffix(m.GatewayURL, "/") + + return &m, nil +} + +func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName string) (string, error) { + describeItem := akeyless.NewDescribeItem(secretName) + describeItem.SetToken(a.token) + describeItemResp, _, err := a.v2.DescribeItem(ctx).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(ctx context.Context, secretName string, secretType string) (string, error) { + + var secretValue string + var err error + + switch secretType { + case STATIC_SECRET_RESPONSE: + getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) + getSecretValue.SetToken(a.token) + 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 + } + + // 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 DYNAMIC_SECRET_RESPONSE: + getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) + getDynamicSecretValue.SetToken(a.token) + 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 + } + + // 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(jsonBytes, &dynamicSecretResp); unmarshalErr != nil { + err = fmt.Errorf("failed to unmarshal secret response: %w", unmarshalErr) + break + } + + // Check if the response contains an error + if dynamicSecretResp.Error != "" { + err = fmt.Errorf("dynamic secret retrieval error: %s", dynamicSecretResp.Error) + break + } + + // Return the value field directly (already a JSON string with credentials) + secretValue = dynamicSecretResp.Value + + case ROTATED_SECRET_RESPONSE: + getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) + getRotatedSecretValue.SetToken(a.token) + 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 + } + + // Marshal the entire response value object + jsonBytes, marshalErr := json.Marshal(secretRespMap) + if marshalErr != nil { + err = fmt.Errorf("failed to marshal rotated 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(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(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 { + 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(ctx context.Context, path string, types []string) ([]akeyless.Item, error) { + var allItems []akeyless.Item + + // Create the list items request + listItems := akeyless.NewListItems() + listItems.SetToken(a.token) + listItems.SetPath(path) + listItems.SetAutoPagination("enabled") + listItems.SetType(types) + + // Execute the list items request + a.logger.Debugf("listing items from path '%s'...", path) + itemsList, _, err := a.v2.ListItems(ctx).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(ctx, folder, types) + if err != nil { + return nil, err + } + allItems = append(allItems, subItems...) + } + } + + return allItems, nil +} + +func (a *akeylessSecretStore) separateItemsByType(items []akeyless.Item) ([]string, []string, []string) { + var staticItems []akeyless.Item + var dynamicItems []akeyless.Item + var rotatedItems []akeyless.Item + for _, item := range items { + itemType := *item.ItemType + + switch itemType { + case STATIC_SECRET_RESPONSE: + staticItems = append(staticItems, item) + case DYNAMIC_SECRET_RESPONSE: + dynamicItems = append(dynamicItems, item) + case ROTATED_SECRET_RESPONSE: + rotatedItems = append(rotatedItems, item) + } + } + + // listItems can get quite large, so we don't need all item details, we can use the item names instead + // and free memory + items = nil + staticItemNames := getItemNames(staticItems) + dynamicItemNames := getItemNames(dynamicItems) + rotatedItemNames := getItemNames(rotatedItems) + a.logger.Debugf("static items: %v", staticItemNames) + a.logger.Debugf("dynamic items: %v", dynamicItemNames) + a.logger.Debugf("rotated items: %v", rotatedItemNames) + + return staticItemNames, dynamicItemNames, rotatedItemNames +} + +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 +} + +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 new file mode 100644 index 0000000000..ad8916fb98 --- /dev/null +++ b/secretstores/akeyless/akeyless_test.go @@ -0,0 +1,1883 @@ +package akeyless + +import ( + "context" + "encoding/json" + "fmt" + "io" + "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" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + testAccessIdIAM = "p-xt3sT2nah7gpwm" + testAccessIdJwt = "p-xt3sT2nah7gpom" + testAccessIdKey = "p-xt3sT2nah7gpam" + testAccessKey = "ABCD1233xxx=" + // { + // "sub": "1234567890", + // "name": "John Doe", + // "iat": 1516239022 + // } + testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QeJkP5vWKT_yUZJgIeUAnYw2brk" + testSecretValue = "r3vE4L3D" +) + +var ( + 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 = STATIC_SECRET_RESPONSE + mockDescribeStaticSecretItemResponse = akeyless.Item{ + 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{ + mockStaticSecretJSONName: { + "some": "json", + }, + } + mockStaticSecretJSONItemResponse = akeyless.Item{ + 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{ + mockStaticSecretPasswordName: { + "password": testSecretValue, + "username": "akeyless", + }, + } + mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) + mockDescribeDynamicSecretType = DYNAMIC_SECRET_RESPONSE + mockDescribeDynamicSecretItemResponse = akeyless.Item{ + 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 = 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 = ROTATED_SECRET_RESPONSE + mockDescribeRotatedSecretItemResponse = akeyless.Item{ + 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 = map[string]interface{}{ + "value": map[string]interface{}{ + "username": "abcdefghijklmnopqrstuvwxyz", + "password": testSecretValue, + "application_id": "1234567890", + }, + } + mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretJSONItemName) + mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ + mockStaticSecretJSONName: { + "some": "json", + }, + } + mockStaticSecretJSONItemResponse = akeyless.Item{ + 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{ + mockStaticSecretPasswordName: { + "password": testSecretValue, + "username": "akeyless", + }, + } + mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) + mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE + mockDescribeDynamicSecretItemResponse = akeyless.Item{ + 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 = 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 = ROTATED_SECRET_RESPONSE + mockDescribeRotatedSecretItemResponse = akeyless.Item{ + 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 = map[string]interface{}{ + "value": map[string]interface{}{ + "username": "abcdefghijklmnopqrstuvwxyz", + "password": testSecretValue, + "application_id": "1234567890", + }, + } +) + +var mockGetSingleSecretValueResponse = map[string]string{ + mockDescribeStaticSecretName: testSecretValue, +} + +// Global mock server for all tests +var mockGateway *httptest.Server + +// mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID +func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + authRequest := akeyless.NewAuth() + authRequest.SetAccessId(metadata.AccessID) + + authRequest.SetAccessKey(metadata.AccessKey) + + config := akeyless.NewConfiguration() + config.Servers = []akeyless.ServerConfiguration{ + { + URL: metadata.GatewayURL, + }, + } + config.UserAgent = USER_AGENT + config.AddDefaultHeader("akeylessclienttype", USER_AGENT) + + akeylessSecretStore.v2 = akeyless.NewAPIClient(config).V2Api + + out, _, err := akeylessSecretStore.v2.Auth(context.Background()).Body(*authRequest).Execute() + if err != nil { + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + + 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": + // 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(mockGetSingleSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/list-items": + listItemsResponse := akeyless.NewListItemsInPathOutput() + listItemsResponse.SetItems( + []akeyless.Item{mockDescribeStaticSecretItemResponse}, + ) + jsonResponse, _ := json.Marshal(listItemsResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) + 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": + listItemsResponse := akeyless.NewListItemsInPathOutput() + listItemsResponse.SetItems( + []akeyless.Item{mockDescribeStaticSecretItemResponse}, + ) + jsonResponse, _ := json.Marshal(listItemsResponse) + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + case "/describe-item": + jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) + 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() + + // Exit with the same code as the tests + os.Exit(code) +} + +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: "gw, access id and key", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "gw, access id and jwt", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, + }, + }, + }, + expectError: false, + }, + { + name: "gw, access id (aws_iam)", + metadata: secretstores.Metadata{ + Base: metadata.Base{ + 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, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + log := logger.NewLogger("test") + store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + + 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 { + // 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) + } + } + }) + } +} + +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 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 access id and key", + properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdKey, + AccessKey: testAccessKey, + GatewayURL: "https://api.akeyless.io", // Default gateway URL + }, + }, + { + name: "valid metadata with access id and jwt", + properties: map[string]string{ + "accessId": testAccessIdJwt, + "jwt": testJWT, + "gatewayUrl": mockGateway.URL, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdJwt, + JWT: testJWT, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "valid metadata with access id aws_iam", + properties: map[string]string{ + "accessId": testAccessIdIAM, + "gatewayUrl": mockGateway.URL, + }, + expectError: false, + expected: &akeylessMetadata{ + AccessID: testAccessIdIAM, + GatewayURL: mockGateway.URL, + }, + }, + { + name: "missing access id", + properties: map[string]string{ + "gatewayUrl": mockGateway.URL, + }, + expectError: true, + }, + { + name: "invalid gateway url", + properties: map[string]string{ + "gatewayUrl": "http:/invalidaddress", + }, + expectError: true, + }, + { + name: "invalid access id format", + properties: map[string]string{ + "accessId": "invalid", + }, + 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 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) + + // 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) +} + +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: "test text single static secret", + request: secretstores.GetSecretRequest{ + Name: mockDescribeStaticSecretName, + }, + expectError: false, + expectedSecret: testSecretValue, + }, + // { + // name: "get non-existing secret", + // request: secretstores.GetSecretRequest{ + // Name: mockDescribeStaticSecretName, + // }, + // expectError: true, + // expectedSecret: "", + // }, + } + + 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 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() +} + +// Test GetSecretType functions +func TestGetSecretType(t *testing.T) { + // Test GetSecretType + store := NewAkeylessSecretStore(logger.NewLogger("test")).(*akeylessSecretStore) + meta := secretstores.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{ + "accessId": testAccessIdKey, + "accessKey": testAccessKey, + "gatewayUrl": mockGateway.URL, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + + secretType, err := store.GetSecretType(ctx, mockDescribeStaticSecretName) + assert.NoError(t, err) + assert.Equal(t, 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, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + 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") + + // 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, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + + 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) + + 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{ + mockDescribeStaticSecretItemResponse, + mockStaticSecretJSONItemResponse, + mockDescribeDynamicSecretItemResponse, + mockDescribeRotatedSecretItemResponse, + }, + ) + jsonResponse, _ := json.Marshal(&items) + 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 "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + 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 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 := "{\"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, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", response.Data[rotatedSecretKey][rotatedSecretKey]) + + 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, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem2 := akeyless.Item{ + ItemName: &staticSecret2, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem3 := akeyless.Item{ + ItemName: &staticSecret3, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem1 := akeyless.Item{ + ItemName: &dynamicSecret1, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem2 := akeyless.Item{ + ItemName: &dynamicSecret2, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + rotatedItem1 := akeyless.Item{ + ItemName: &rotatedSecret1, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedStaticItem := akeyless.Item{ + ItemName: &mixedStaticSecret, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedDynamicItem := akeyless.Item{ + ItemName: &mixedDynamicSecret, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedRotatedItem := akeyless.Item{ + 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) { + 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 + 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 + 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) + + 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, + IsEnabled: func(b bool) *bool { return &b }(true), + } + case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: + itemResponse = akeyless.Item{ + 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, + 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) + 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 := "{\"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 := "{\"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 := "{\"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 := "{\"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 := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" + assert.Equal(t, expectedMixedRotatedValue, response.Data[mixedRotatedSecret][mixedRotatedSecret]) + + mockGateway.Close() +} + +func TestParseSecretTypes(t *testing.T) { + tests := []struct { + name string + input string + expected []string + expectError bool + }{ + { + name: "all", + input: "all", + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "static", + input: "static", + expected: []string{STATIC_SECRET_TYPE}, + }, + { + name: "dynamic", + input: "dynamic", + expected: []string{DYNAMIC_SECRET_TYPE}, + }, + { + name: "rotated", + input: "rotated", + expected: []string{ROTATED_SECRET_TYPE}, + }, + { + name: "static,dynamic", + input: "static,dynamic", + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE}, + }, + { + name: "static,dynamic,rotated", + input: "static,dynamic,rotated", + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "invalid", + input: "invalid", + expectError: true, + }, + { + name: "empty", + input: "", + expectError: false, + expected: supportedSecretTypes, + }, + { + name: "mixed case", + input: "Static,Dynamic,ROTATED", + expectError: false, + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "duplicates", + input: "static-secret,dynamic-secret,static-secret", + expectError: false, + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE}, + }, + { + name: "mixed sdk format and direct format", + input: "static-secret,dynamic-secret,rotated-secret,static", + expectError: false, + expected: []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE}, + }, + { + name: "invalid type", + input: "invalid", + expectError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parsedTypes, err := parseSecretTypes(tt.input) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expected, parsedTypes) + } + }) + } +} + +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, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + 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") + + // 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, + }, + }, + } + + ctx := context.Background() + err := store.Init(ctx, meta) + require.NoError(t, err) + + 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) + + 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{ + mockDescribeStaticSecretItemResponse, + mockStaticSecretJSONItemResponse, + mockDescribeDynamicSecretItemResponse, + mockDescribeRotatedSecretItemResponse, + }, + ) + jsonResponse, _ := json.Marshal(&items) + 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 "/get-rotated-secret-value": + jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) + 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 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 := "{\"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, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", response.Data[rotatedSecretKey][rotatedSecretKey]) + + 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, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem2 := akeyless.Item{ + ItemName: &staticSecret2, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + staticItem3 := akeyless.Item{ + ItemName: &staticSecret3, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem1 := akeyless.Item{ + ItemName: &dynamicSecret1, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + dynamicItem2 := akeyless.Item{ + ItemName: &dynamicSecret2, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + rotatedItem1 := akeyless.Item{ + ItemName: &rotatedSecret1, + ItemType: &mockDescribeRotatedSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedStaticItem := akeyless.Item{ + ItemName: &mixedStaticSecret, + ItemType: &mockDescribeStaticSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedDynamicItem := akeyless.Item{ + ItemName: &mixedDynamicSecret, + ItemType: &mockDescribeDynamicSecretType, + IsEnabled: func(b bool) *bool { return &b }(true), + } + mixedRotatedItem := akeyless.Item{ + 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) { + 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 + 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 + 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) + + 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, + IsEnabled: func(b bool) *bool { return &b }(true), + } + case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: + itemResponse = akeyless.Item{ + 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, + 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) + 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 := "{\"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 := "{\"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 := "{\"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 := "{\"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 := "{\"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/metadata.yaml b/secretstores/akeyless/metadata.yaml new file mode 100644 index 0000000000..a4ae1b1227 --- /dev/null +++ b/secretstores/akeyless/metadata.yaml @@ -0,0 +1,78 @@ +# 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 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: accessId + required: true + description: | + 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 + - 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: "http://gw.akeyless.svc.cluster.local:8000" + 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 + - 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 diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go new file mode 100644 index 0000000000..5d7c2a0967 --- /dev/null +++ b/secretstores/akeyless/utils.go @@ -0,0 +1,266 @@ +package akeyless + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "regexp" + "strings" + + "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. +const ( + 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" + STATIC_SECRET_TYPE = "static-secret" + DYNAMIC_SECRET_TYPE = "dynamic-secret" + ROTATED_SECRET_TYPE = "rotated-secret" + ALL_SECRET_TYPES = "all" + CLIENT_SOURCE = "akeylessclienttype" + PATH_DEFAULT = "/" + METADATA_PATH_KEY = "path" + METADATA_SECRETS_TYPE_KEY = "secrets_type" +) + +var supportedSecretTypes = []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE} + +// AccessTypeCharMap maps single-character access types to their display names. +var accessTypeCharMap = map[string]string{ + "a": DEFAULT_AUTH_TYPE, + "o": AUTH_JWT, + "w": AUTH_IAM, + "k": AUTH_K8S, +} + +// 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 +} + +// 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 +} + +func getDaprSingleSecretResponse(secretName string, secretValue string) (secretstores.GetSecretResponse, error) { + return secretstores.GetSecretResponse{ + Data: map[string]string{ + secretName: secretValue, + }, + }, nil +} + +func getItemNames(items []akeyless.Item) []string { + itemNames := []string{} + for _, item := range items { + itemNames = append(itemNames, *item.ItemName) + } + return itemNames +} + +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 +} + +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 STATIC_SECRET_RESPONSE: + logger.Debugf("static secret '%s' is active", *secret.ItemName) + isActive = true + case 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 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" || status == "RotationInitialStatus" { + 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 +} + +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 +} + +// `parseSecretTypes` parses the `secret_types` metadata parameter +// and returns a slice of supported secret types in the format expected +// by the Akeyless `POST /list-items` API. +// It accepts a comma-separated string of secret types and returns a slice of supported secret types. +func parseSecretTypes(secretTypes string) ([]string, error) { + // Handle "all" or empty string which returns all supported secret types + if secretTypes == ALL_SECRET_TYPES || secretTypes == "" { + return supportedSecretTypes, nil + } + + // Parse comma-separated values + types := strings.Split(secretTypes, ",") + if len(types) == 0 { + return nil, fmt.Errorf("no secret types provided") + } + result := make([]string, 0, len(types)) + + // Map metadata.secret_types to supportedSecretTypes + typeMap := map[string]string{ + "static": STATIC_SECRET_TYPE, + "dynamic": DYNAMIC_SECRET_TYPE, + "rotated": ROTATED_SECRET_TYPE, + } + + for _, t := range types { + t = strings.ToLower(strings.TrimSpace(t)) + if mappedType, ok := typeMap[t]; ok { + result = append(result, mappedType) + } else { + // Allow direct SDK format + if t == STATIC_SECRET_TYPE || t == DYNAMIC_SECRET_TYPE || t == ROTATED_SECRET_TYPE { + result = append(result, t) + } else { + return nil, fmt.Errorf("invalid secret type '%s', supported types: static[-secret], dynamic[-secret], rotated[-secret]", t) + } + } + } + + // Dedup + seen := make(map[string]bool) + unique := []string{} + for _, t := range result { + if !seen[t] { + seen[t] = true + unique = append(unique, t) + } + } + + return unique, nil +} From 07b2a96d2a13f89ecc74fbcb4137d3f8e3988507 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 5 Dec 2025 10:41:18 -0500 Subject: [PATCH 02/19] rm dup func Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 617ad367e2..49757980e6 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -554,16 +554,3 @@ func (a *akeylessSecretStore) filterInactiveSecrets(secrets []akeyless.Item) []a return filteredSecrets } - -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 -} From 9a5ee65c371b9dac78af1c0832087b338e4f63d6 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 5 Dec 2025 10:47:34 -0500 Subject: [PATCH 03/19] cleaned up tests Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless_test.go | 621 ------------------------- 1 file changed, 621 deletions(-) diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index ad8916fb98..d2f2c41354 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -98,59 +98,6 @@ var ( "application_id": "1234567890", }, } - mockStaticSecretJSONName = fmt.Sprintf("/path/to/akeyless%s", mockStaticSecretJSONItemName) - mockGetSingleSecretJSONValueResponse = map[string]map[string]string{ - mockStaticSecretJSONName: { - "some": "json", - }, - } - mockStaticSecretJSONItemResponse = akeyless.Item{ - 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{ - mockStaticSecretPasswordName: { - "password": testSecretValue, - "username": "akeyless", - }, - } - mockDescribeDynamicSecretName = fmt.Sprintf("/path/to/akeyless%s", mockDynamicSecretItemName) - mockDescribeDynamicSecretType = AKEYLESS_SECRET_TYPE_DYNAMIC_SECRET_RESPONSE - mockDescribeDynamicSecretItemResponse = akeyless.Item{ - 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 = 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 = ROTATED_SECRET_RESPONSE - mockDescribeRotatedSecretItemResponse = akeyless.Item{ - 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 = map[string]interface{}{ - "value": map[string]interface{}{ - "username": "abcdefghijklmnopqrstuvwxyz", - "password": testSecretValue, - "application_id": "1234567890", - }, - } ) var mockGetSingleSecretValueResponse = map[string]string{ @@ -213,46 +160,6 @@ func TestMain(m *testing.M) { jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) - case "/list-items": - listItemsResponse := akeyless.NewListItemsInPathOutput() - listItemsResponse.SetItems( - []akeyless.Item{mockDescribeStaticSecretItemResponse}, - ) - jsonResponse, _ := json.Marshal(listItemsResponse) - w.WriteHeader(http.StatusOK) - w.Write(jsonResponse) - case "/describe-item": - jsonResponse, _ := json.Marshal(mockDescribeStaticSecretItemResponse) - 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": listItemsResponse := akeyless.NewListItemsInPathOutput() listItemsResponse.SetItems( @@ -419,22 +326,6 @@ func TestClose(t *testing.T) { assert.NoError(t, err) } -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 @@ -1369,515 +1260,3 @@ func TestParseSecretTypes(t *testing.T) { }) } } - -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, - }, - }, - } - - ctx := context.Background() - err := store.Init(ctx, meta) - require.NoError(t, err) - 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") - - // 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, - }, - }, - } - - ctx := context.Background() - err := store.Init(ctx, meta) - require.NoError(t, err) - - 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) - - 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{ - mockDescribeStaticSecretItemResponse, - mockStaticSecretJSONItemResponse, - mockDescribeDynamicSecretItemResponse, - mockDescribeRotatedSecretItemResponse, - }, - ) - jsonResponse, _ := json.Marshal(&items) - 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 "/get-rotated-secret-value": - jsonResponse, _ := json.Marshal(&mockGetSingleRotatedSecretValueResponse) - 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 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 := "{\"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, "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"r3vE4L3D\",\"username\":\"abcdefghijklmnopqrstuvwxyz\"}}", response.Data[rotatedSecretKey][rotatedSecretKey]) - - 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, - IsEnabled: func(b bool) *bool { return &b }(true), - } - staticItem2 := akeyless.Item{ - ItemName: &staticSecret2, - ItemType: &mockDescribeStaticSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - staticItem3 := akeyless.Item{ - ItemName: &staticSecret3, - ItemType: &mockDescribeStaticSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - dynamicItem1 := akeyless.Item{ - ItemName: &dynamicSecret1, - ItemType: &mockDescribeDynamicSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - dynamicItem2 := akeyless.Item{ - ItemName: &dynamicSecret2, - ItemType: &mockDescribeDynamicSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - rotatedItem1 := akeyless.Item{ - ItemName: &rotatedSecret1, - ItemType: &mockDescribeRotatedSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - mixedStaticItem := akeyless.Item{ - ItemName: &mixedStaticSecret, - ItemType: &mockDescribeStaticSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - mixedDynamicItem := akeyless.Item{ - ItemName: &mixedDynamicSecret, - ItemType: &mockDescribeDynamicSecretType, - IsEnabled: func(b bool) *bool { return &b }(true), - } - mixedRotatedItem := akeyless.Item{ - 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) { - 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 - 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 - 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) - - 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, - IsEnabled: func(b bool) *bool { return &b }(true), - } - case dynamicSecret1, dynamicSecret2, mixedDynamicSecret: - itemResponse = akeyless.Item{ - 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, - 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) - 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 := "{\"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 := "{\"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 := "{\"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 := "{\"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 := "{\"value\":{\"application_id\":\"1234567890\",\"password\":\"rotated-secret-1-value\",\"username\":\"rotated-user\"}}" - assert.Equal(t, expectedMixedRotatedValue, response.Data[mixedRotatedSecret][mixedRotatedSecret]) - - mockGateway.Close() -} From a86ca402ce44b35b8b4f6adeebb2b996d64b0378 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Mon, 8 Dec 2025 15:35:45 -0500 Subject: [PATCH 04/19] handle nil response when auth Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 49757980e6..42cc429bc1 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -124,8 +124,20 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle 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 (HTTP status code: %d): %w", httpResponse.StatusCode, errors.New(httpResponse.Status)) + if err != nil { + if httpResponse != nil { + return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %w", httpResponse.StatusCode, err) + } + return fmt.Errorf("failed to authenticate with Akeyless: %w", err) + } + if httpResponse == nil || httpResponse.StatusCode != 200 { + statusCode := 0 + status := "unknown" + if httpResponse != nil { + statusCode = httpResponse.StatusCode + status = httpResponse.Status + } + return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %s", statusCode, status) } a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) From ae8999eacb3db66822d5403ec8bc6a0ec32522cb Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Tue, 9 Dec 2025 10:35:12 -0500 Subject: [PATCH 05/19] add gw tls conf Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 9 ++++++--- secretstores/akeyless/akeyless.go | 18 +++++++++++++++++ secretstores/akeyless/metadata.yaml | 8 ++++++++ secretstores/akeyless/utils.go | 30 +++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 3 deletions(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index da736115b6..74e9ce4eea 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -18,6 +18,7 @@ The Akeyless secret store component supports the following configuration options | Field | Required | Description | Example | |-------|----------|-------------|---------| | `gatewayUrl` | No | The Akeyless Gateway URL. Default is https://api.akeyless.io. | `https://your-gateway.akeyless.io` | +| `gatewayTLSCA` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. | `LS0tLS1CRUdJTi...` | | `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...=` | @@ -48,6 +49,8 @@ spec: metadata: - name: gatewayUrl value: "https://your-gateway.akeyless.io" + - name: gatewayTLSCA + value: "LS0tLS1CRUdJTi...." - name: accessId value: "p-1234Abcdam" - name: accessKey @@ -67,7 +70,7 @@ spec: version: v1 metadata: - name: gatewayUrl - value: "https://your-gateway.akeyless.io" + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" - name: accessId value: "p-1234Abcdom" - name: jwt @@ -86,7 +89,7 @@ spec: version: v1 metadata: - name: gatewayUrl - value: "https://your-gateway.akeyless.io" + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" - name: accessId value: "p-1234Abcdwm" ``` @@ -103,7 +106,7 @@ spec: version: v1 metadata: - name: gatewayUrl - value: "https://gw.akeyless.svc.cluster.local" + value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" - name: accessId value: "p-1234Abcdwm" - name: k8sAuthConfigName diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 42cc429bc1..8d4471b495 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" "net/url" "strings" "sync" @@ -36,6 +37,7 @@ 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"` + GatewayTLSCA string `json:"gatewayTLSCA" mapstructure:"gatewayTLSCA"` JWT string `json:"jwt" mapstructure:"jwt"` AccessID string `json:"accessId" mapstructure:"accessId"` AccessKey string `json:"accessKey" mapstructure:"accessKey"` @@ -120,6 +122,22 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle config.UserAgent = USER_AGENT config.AddDefaultHeader(CLIENT_SOURCE, USER_AGENT) + // Configure TLS if gatewayTLSCA is provided + if metadata.GatewayTLSCA != "" { + a.logger.Debug("configuring TLS for Akeyless client...") + tlsConfig, err := createTLSConfig(metadata.GatewayTLSCA) + if err != nil { + return errors.New("failed to create TLS configuration: " + err.Error()) + } + + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + config.HTTPClient = httpClient + } + a.v2 = akeyless.NewAPIClient(config).V2Api a.logger.Debug("authenticating with Akeyless...") diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml index a4ae1b1227..098f5d1034 100644 --- a/secretstores/akeyless/metadata.yaml +++ b/secretstores/akeyless/metadata.yaml @@ -16,6 +16,14 @@ metadata: default: "https://api.akeyless.io" example: "https://your.akeyless.gw" type: string + - name: gatewayTLSCA + required: false + description: | + base64-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway + with a self-signed or custom CA certificate. + example: "LS0tLS1CRUdJTi..." + type: string + sensitive: true - name: accessId required: true description: | diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 5d7c2a0967..391e9c5f1a 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -1,7 +1,11 @@ package akeyless import ( + "crypto/tls" + "crypto/x509" + "encoding/base64" "encoding/json" + "encoding/pem" "errors" "fmt" "os" @@ -264,3 +268,29 @@ func parseSecretTypes(secretTypes string) ([]string, error) { return unique, nil } + +func createTLSConfig(gatewayTLSCA string) (*tls.Config, error) { + + // Decode base64 to PEM + certBytes, err := base64.StdEncoding.DecodeString(gatewayTLSCA) + if err != nil { + return nil, fmt.Errorf("failed to decode base64-encoded gateway TLS CA: %w", err) + } + + // Validate PEM format + block, _ := pem.Decode(certBytes) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM certificate: invalid PEM format") + } + + // Cereate cert pool and add certificate + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(certBytes) { + return nil, errors.New("failed to add certificate to cert pool") + } + + return &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: caCertPool, + }, nil +} From 109dc0ae481aeedee3f5eb0f9dd9447a524b400d Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Tue, 9 Dec 2025 12:35:27 -0500 Subject: [PATCH 06/19] set access type for auth request Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 8d4471b495..59a5364369 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -94,17 +94,20 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle } authRequest.SetAccessKey(metadata.AccessKey) case AUTH_IAM: + authRequest.SetAccessType(AUTH_IAM) id, err := aws.GetCloudId() if err != nil { return errors.New("unable to get cloud ID: " + err.Error()) } authRequest.SetCloudId(id) case AUTH_JWT: + authRequest.SetAccessType(AUTH_JWT) if metadata.JWT == "" { return errors.New("jwt is required for JWT authentication") } authRequest.SetJwt(metadata.JWT) case AUTH_K8S: + authRequest.SetAccessType(AUTH_K8S) err := setK8SAuthConfiguration(*metadata, authRequest, a) if err != nil { return errors.New("failed to set k8s auth configuration: " + err.Error()) From 2f83dc86c72a2933f99c0fb49a006d3eac3793e2 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Tue, 9 Dec 2025 14:18:02 -0500 Subject: [PATCH 07/19] align k8s auth conf parameters (encode jwt, trim v2 endpoint Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 6 +++--- secretstores/akeyless/utils.go | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index 74e9ce4eea..e3bf13bdb0 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -17,7 +17,7 @@ The Akeyless secret store component supports the following configuration options | Field | Required | Description | Example | |-------|----------|-------------|---------| -| `gatewayUrl` | No | The Akeyless Gateway URL. Default is https://api.akeyless.io. | `https://your-gateway.akeyless.io` | +| `gatewayUrl` | No | The Akeyless Gateway API URL. Default is https://api.akeyless.io. | `https://gw.akeyless.svc.cluster.local:8000/api/v2` | | `gatewayTLSCA` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. | `LS0tLS1CRUdJTi...` | | `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...` | @@ -25,7 +25,7 @@ The Akeyless secret store component supports the following configuration options | `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 will try to read it from the default service account token file `/var/run/secrets/kubernetes.io/serviceaccount/token`. | `eyJ...` | @@ -108,7 +108,7 @@ spec: - name: gatewayUrl value: "http://unified.akeyless.svc.cluster.local:8000/api/v2" - name: accessId - value: "p-1234Abcdwm" + value: "p-1234Abcdkm" - name: k8sAuthConfigName value: "us-east-1-prod-akeyless-k8s-conf" - name: k8sGatewayUrl diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 391e9c5f1a..9dc3625957 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -209,12 +209,20 @@ func setK8SAuthConfiguration(metadata akeylessMetadata, authRequest *akeyless.Au } metadata.K8sServiceAccountToken = string(token) } + + // base64 encode the token if it's not already encoded + if _, err := base64.StdEncoding.DecodeString(metadata.K8sServiceAccountToken); err != nil { + a.logger.Info("k8sServiceAccountToken is not base64 encoded, encoding it...") + metadata.K8sServiceAccountToken = base64.StdEncoding.EncodeToString([]byte(metadata.K8sServiceAccountToken)) + } + authRequest.SetK8sServiceAccountToken(metadata.K8sServiceAccountToken) + if metadata.K8SGatewayURL == "" { a.logger.Debug("k8s gateway url is missing, using gatewayUrl") metadata.K8SGatewayURL = metadata.GatewayURL } + metadata.K8SGatewayURL = strings.TrimSuffix(metadata.K8SGatewayURL, "/api/v2") authRequest.SetGatewayUrl(metadata.K8SGatewayURL) - authRequest.SetK8sServiceAccountToken(metadata.K8sServiceAccountToken) return nil } From cdc3f945756bf110d81cef9fd11441690e892ec2 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Tue, 9 Dec 2025 15:34:01 -0500 Subject: [PATCH 08/19] fix zero-value init of secret response Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 2 +- secretstores/akeyless/utils.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 59a5364369..6e33bfe6c8 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -490,7 +490,7 @@ func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretNa // It returns a map of secret names and their values. func (a *akeylessSecretStore) GetBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { - var secretResponse = make([]secretResultCollection, len(secretNames)) + var secretResponse []secretResultCollection getSecretsValues := akeyless.NewGetSecretValue(secretNames) getSecretsValues.SetToken(a.token) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 9dc3625957..f481ffe782 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -17,7 +17,6 @@ import ( "github.com/dapr/kit/logger" ) -// Define constants for the access types. These are equivalent to the TypeScript consts. const ( AUTH_JWT = "jwt" DEFAULT_AUTH_TYPE = "access_key" From 5682fd3bcda70faf2d3f669f730d43d62a9e8110 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Tue, 9 Dec 2025 16:58:43 -0500 Subject: [PATCH 09/19] added token refresh support Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 309 +++++++++++++++++++++++-- secretstores/akeyless/akeyless_test.go | 61 ++++- secretstores/akeyless/utils.go | 57 +++-- 3 files changed, 387 insertions(+), 40 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 6e33bfe6c8..9c394489c2 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -9,6 +9,7 @@ import ( "net/url" "strings" "sync" + "time" aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" "github.com/akeylesslabs/akeyless-go/v5" @@ -22,9 +23,14 @@ var _ secretstores.SecretStore = (*akeylessSecretStore)(nil) // akeylessSecretStore is a secret store implementation for Akeyless. type akeylessSecretStore struct { - v2 *akeyless.V2ApiService - token string - logger logger.Logger + v2 *akeyless.V2ApiService + token string + tokenExpiry time.Time + metadata *akeylessMetadata + mu sync.RWMutex + logger logger.Logger + closeCh chan struct{} + wg sync.WaitGroup } // NewAkeylessSecretStore returns a new Akeyless secret store. @@ -55,11 +61,19 @@ func (a *akeylessSecretStore) Init(ctx context.Context, meta secretstores.Metada return errors.New("failed to parse metadata: " + err.Error()) } + a.metadata = m + a.closeCh = make(chan struct{}) + err = a.authenticate(ctx, m) if err != nil { return errors.New("failed to authenticate with Akeyless: " + err.Error()) } + // Start background token refresh routine if we have expiration time + if !a.tokenExpiry.IsZero() { + a.startTokenRefreshRoutine(ctx, m) + } + return nil } @@ -162,7 +176,25 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle } a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) + + // Store token and expiration with mutex protection + a.mu.Lock() a.token = out.GetToken() + expirationStr := out.GetExpiration() + a.mu.Unlock() + + // Parse and store expiration time + if expirationStr != "" { + expiration, err := parseTokenExpirationDate(expirationStr) + if err != nil { + a.logger.Warnf("failed to parse token expiration '%s': %v", expirationStr, err) + } else { + a.mu.Lock() + a.tokenExpiry = expiration + a.mu.Unlock() + a.logger.Debugf("token expiration parsed and set successfully: %s", expiration.Format(time.RFC3339)) + } + } return nil } @@ -346,6 +378,10 @@ func (a *akeylessSecretStore) Features() []secretstores.Feature { // Close closes the secret store. func (a *akeylessSecretStore) Close() error { + if a.closeCh != nil { + close(a.closeCh) + a.wg.Wait() + } return nil } @@ -386,9 +422,34 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle } func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName string) (string, error) { + + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to ensure valid token: %w", err) + } + describeItem := akeyless.NewDescribeItem(secretName) - describeItem.SetToken(a.token) - describeItemResp, _, err := a.v2.DescribeItem(ctx).Body(*describeItem).Execute() + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + describeItem.SetToken(token) + describeItemResp, httpResponse, err := a.v2.DescribeItem(ctx).Body(*describeItem).Execute() + + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debug("received 401 unauthorized, re-authenticating...") + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to re-authenticate after 401: %w", err) + } + + a.mu.RLock() + token = a.token + a.mu.RUnlock() + + describeItem.SetToken(token) + describeItemResp, _, err = a.v2.DescribeItem(ctx).Body(*describeItem).Execute() + } + if err != nil { return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) } @@ -404,14 +465,60 @@ func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName stri // It returns the value of the secret or an error if the secret is not found. func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretName string, secretType string) (string, error) { + if err := a.ensureValidToken(ctx); err != nil { + return "", fmt.Errorf("failed to ensure valid token: %w", err) + } + var secretValue string var err error + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + // Helper to get current token (with mutex protection) + getToken := func() string { + a.mu.RLock() + defer a.mu.RUnlock() + return a.token + } + + retry := func(apiCall func() error, updateToken func(string)) error { + apiErr := apiCall() + if apiErr != nil { + // Check if it's a 401 error by examining the error string or response + if strings.Contains(apiErr.Error(), "401") || strings.Contains(apiErr.Error(), "Unauthorized") { + a.logger.Debug("received 401 unauthorized, re-authenticating...") + if reauthErr := a.ensureValidToken(ctx); reauthErr != nil { + return fmt.Errorf("failed to re-authenticate after 401: %w", reauthErr) + } + // Update token in the request object before retry + newToken := getToken() + updateToken(newToken) + return apiCall() + } + } + return apiErr + } + switch secretType { case STATIC_SECRET_RESPONSE: getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) - getSecretValue.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() + getSecretValue.SetToken(token) + var secretRespMap map[string]interface{} + var httpResponse *http.Response + + apiErr := retry(func() error { + var err error + secretRespMap, httpResponse, err = a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("401 Unauthorized") + } + return err + }, func(newToken string) { + getSecretValue.SetToken(newToken) + }) + if apiErr != nil { err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) break @@ -434,8 +541,21 @@ func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretNa case DYNAMIC_SECRET_RESPONSE: getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) - getDynamicSecretValue.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetDynamicSecretValue(ctx).Body(*getDynamicSecretValue).Execute() + getDynamicSecretValue.SetToken(token) + var secretRespMap map[string]interface{} + var httpResponse *http.Response + + apiErr := retry(func() error { + var err error + secretRespMap, httpResponse, err = a.v2.GetDynamicSecretValue(ctx).Body(*getDynamicSecretValue).Execute() + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("401 Unauthorized") + } + return err + }, func(newToken string) { + getDynamicSecretValue.SetToken(newToken) + }) + if apiErr != nil { err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) break @@ -467,8 +587,21 @@ func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretNa case ROTATED_SECRET_RESPONSE: getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) - getRotatedSecretValue.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetRotatedSecretValue(ctx).Body(*getRotatedSecretValue).Execute() + getRotatedSecretValue.SetToken(token) + var secretRespMap map[string]interface{} + var httpResponse *http.Response + + apiErr := retry(func() error { + var err error + secretRespMap, httpResponse, err = a.v2.GetRotatedSecretValue(ctx).Body(*getRotatedSecretValue).Execute() + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + return fmt.Errorf("401 Unauthorized") + } + return err + }, func(newToken string) { + getRotatedSecretValue.SetToken(newToken) + }) + if apiErr != nil { err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) break @@ -489,14 +622,46 @@ func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretNa // GetBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. // It returns a map of secret names and their values. func (a *akeylessSecretStore) GetBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { + if err := a.ensureValidToken(ctx); err != nil { + return []secretResultCollection{ + {name: "", value: "", err: fmt.Errorf("failed to ensure valid token: %w", err)}, + } + } var secretResponse []secretResultCollection getSecretsValues := akeyless.NewGetSecretValue(secretNames) - getSecretsValues.SetToken(a.token) - secretRespMap, _, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + getSecretsValues.SetToken(token) + + secretRespMap, httpResponse, apiErr := a.v2.GetSecretValue(ctx).Body(*getSecretsValues).Execute() + + // Handle 401 Unauthorized by re-authenticating and retrying once + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debug("received 401 Unauthorized in bulk get, re-authenticating...") + if err := a.ensureValidToken(ctx); err != nil { + secretResponse = append(secretResponse, secretResultCollection{ + name: "", value: "", err: fmt.Errorf("failed to re-authenticate after 401: %w", err), + }) + return secretResponse + } + + a.mu.RLock() + token = a.token + a.mu.RUnlock() + + getSecretsValues.SetToken(token) + 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)}) + 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) @@ -510,18 +675,44 @@ func (a *akeylessSecretStore) GetBulkStaticSecretValues(ctx context.Context, sec // 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(ctx context.Context, path string, types []string) ([]akeyless.Item, error) { + if err := a.ensureValidToken(ctx); err != nil { + return nil, fmt.Errorf("failed to ensure valid token: %w", err) + } + var allItems []akeyless.Item // Create the list items request listItems := akeyless.NewListItems() - listItems.SetToken(a.token) + + a.mu.RLock() + token := a.token + a.mu.RUnlock() + + listItems.SetToken(token) listItems.SetPath(path) listItems.SetAutoPagination("enabled") listItems.SetType(types) // Execute the list items request a.logger.Debugf("listing items from path '%s'...", path) - itemsList, _, err := a.v2.ListItems(ctx).Body(*listItems).Execute() + itemsList, httpResponse, err := a.v2.ListItems(ctx).Body(*listItems).Execute() + + // Handle 401 Unauthorized by re-authenticating and retrying once + // Check this BEFORE checking err, as 401 might come with an error + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debug("received 401 Unauthorized in list items, re-authenticating...") + if err := a.ensureValidToken(ctx); err != nil { + return nil, fmt.Errorf("failed to re-authenticate after 401: %w", err) + } + + a.mu.RLock() + token = a.token + a.mu.RUnlock() + + listItems.SetToken(token) + itemsList, _, err = a.v2.ListItems(ctx).Body(*listItems).Execute() + } + if err != nil { return nil, err } @@ -587,3 +778,89 @@ func (a *akeylessSecretStore) filterInactiveSecrets(secrets []akeyless.Item) []a return filteredSecrets } + +// ensureValidToken checks if the token is valid and refreshes it if needed (5 minutes before expiration) +// It returns an error if the token refresh fails. +func (a *akeylessSecretStore) ensureValidToken(ctx context.Context) error { + + a.mu.RLock() + expiry := a.tokenExpiry + metadata := a.metadata + a.mu.RUnlock() + + // If token expiry is zero, we can't validate it, so skip validation + // This can happen if expiration parsing failed or wasn't provided + if expiry.IsZero() { + a.logger.Debug("token expiration not set, skipping validation") + return nil + } + + tokenValid := time.Now().Before(expiry.Add(-TOKEN_REFRESH_GRACE_PERIOD)) + if tokenValid { + return nil + } + + // Token expired or about to expire, need to refresh/reauthenticate + a.logger.Debug("token expired or about to expire, reauthenticating...") + a.mu.Lock() + defer a.mu.Unlock() + + // Double-check after acquiring lock (another goroutine might have refreshed) + expiry = a.tokenExpiry + if expiry.IsZero() || time.Now().Before(expiry.Add(-TOKEN_REFRESH_GRACE_PERIOD)) { + return nil + } + + return a.authenticate(ctx, metadata) +} + +// startTokenRefreshRoutine starts a bg goroutine that refreshes the token +func (a *akeylessSecretStore) startTokenRefreshRoutine(ctx context.Context, metadata *akeylessMetadata) { + a.wg.Add(1) + go func() { + defer a.wg.Done() + // Use background context for the refresh routine, not the init context + refreshCtx := context.Background() + + for { + // Check if we should stop first, before acquiring any locks + select { + case <-a.closeCh: + a.logger.Debug("token refresh routine stopped") + return + default: + } + + a.mu.RLock() + expiry := a.tokenExpiry + a.mu.RUnlock() + + if expiry.IsZero() { + a.logger.Warn("token expiration is zero, stopping refresh routine...") + return + } + + refreshDuration := time.Until(expiry.Add(-TOKEN_REFRESH_GRACE_PERIOD)) + if refreshDuration <= 0 { + refreshDuration = time.Minute // Refresh immediately if less than 1 minute left + } + + a.logger.Debugf("next token refresh scheduled in %v", refreshDuration) + + select { + case <-time.After(refreshDuration): + a.logger.Debug("refreshing token...") + if err := a.authenticate(refreshCtx, metadata); err != nil { + a.logger.Errorf("failed to refresh token: %v", err) + // Retry after 1 minute on failure + time.Sleep(time.Minute) + continue + } + a.logger.Debug("token refreshed successfully") + case <-a.closeCh: + a.logger.Debug("token refresh routine stopped") + return + } + } + }() +} diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index d2f2c41354..8df88545ab 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -9,6 +9,7 @@ import ( "net/http/httptest" "os" "testing" + "time" "github.com/akeylesslabs/akeyless-go/v5" "github.com/dapr/components-contrib/metadata" @@ -109,6 +110,11 @@ var mockGateway *httptest.Server // mockAuthenticate is a test version of the Authenticate function that uses a mock cloud ID func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessSecretStore) error { + // Initialize closeCh if not already set + if akeylessSecretStore.closeCh == nil { + akeylessSecretStore.closeCh = make(chan struct{}) + } + authRequest := akeyless.NewAuth() authRequest.SetAccessId(metadata.AccessID) @@ -130,7 +136,23 @@ func mockAuthenticate(metadata *akeylessMetadata, akeylessSecretStore *akeylessS return fmt.Errorf("failed to authenticate with Akeyless: %w", err) } + akeylessSecretStore.mu.Lock() akeylessSecretStore.token = out.GetToken() + expirationStr := out.GetExpiration() + akeylessSecretStore.mu.Unlock() + + // Parse and store expiration time (same as in authenticate) + if expirationStr != "" { + expiration, err := parseTokenExpirationDate(expirationStr) + if err != nil { + // Log warning but don't fail - expiration parsing is optional + akeylessSecretStore.logger.Debugf("failed to parse token expiration '%s': %v", expirationStr, err) + } else { + akeylessSecretStore.mu.Lock() + akeylessSecretStore.tokenExpiry = expiration + akeylessSecretStore.mu.Unlock() + } + } return nil } @@ -147,7 +169,9 @@ func TestMain(m *testing.M) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -253,6 +277,7 @@ func TestInit(t *testing.T) { t.Run(tt.name, func(t *testing.T) { log := logger.NewLogger("test") store := NewAkeylessSecretStore(log).(*akeylessSecretStore) + defer store.Close() // Clean up background goroutine tt.metadata.Properties["gatewayUrl"] = mockGateway.URL @@ -437,6 +462,7 @@ func TestMockServerReturnsAuthOutput(t *testing.T) { assert.NotNil(t, store.v2) assert.NotNil(t, store.token) assert.Equal(t, "t-1234567890", store.token) + defer store.Close() // Clean up background goroutine } func TestMockAWSCloudID(t *testing.T) { @@ -480,6 +506,7 @@ func TestGetSecret(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine tests := []struct { name string @@ -532,7 +559,9 @@ func TestGetSingleSecretJSON(t *testing.T) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -569,6 +598,7 @@ func TestGetSingleSecretJSON(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine response, err := store.GetSecret(context.Background(), secretstores.GetSecretRequest{ Name: mockStaticSecretJSONName, @@ -592,7 +622,9 @@ func TestGetSingleSecretPassword(t *testing.T) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -629,6 +661,7 @@ func TestGetSingleSecretPassword(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine response, err := store.GetSecret(context.Background(), secretstores.GetSecretRequest{ Name: mockStaticSecretPasswordName, @@ -658,6 +691,7 @@ func TestGetSecretType(t *testing.T) { ctx := context.Background() err := store.Init(ctx, meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine secretType, err := store.GetSecretType(ctx, mockDescribeStaticSecretName) assert.NoError(t, err) @@ -675,7 +709,9 @@ func TestGetSingleDynamicSecret(t *testing.T) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -709,6 +745,8 @@ func TestGetSingleDynamicSecret(t *testing.T) { ctx := context.Background() err := store.Init(ctx, meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine + 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) @@ -724,7 +762,9 @@ func TestGetSingleRotatedSecret(t *testing.T) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -758,6 +798,7 @@ func TestGetSingleRotatedSecret(t *testing.T) { ctx := context.Background() err := store.Init(ctx, meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine secretValue, err := store.GetSingleSecretValue(ctx, mockDescribeRotatedSecretName, ROTATED_SECRET_RESPONSE) assert.NoError(t, err) @@ -777,7 +818,9 @@ func TestGetBulkSecretValues(t *testing.T) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -835,6 +878,7 @@ func TestGetBulkSecretValues(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) require.NoError(t, err) @@ -947,7 +991,9 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { // Return a proper AuthOutput JSON response for authentication authOutput := akeyless.NewAuthOutput() authOutput.SetToken("t-1234567890") - authOutput.SetExpiration("2025-01-01T00:00:00Z") + // Use a future expiration date (1 hour from now) to avoid token refresh during tests + futureExpiration := time.Now().Add(1 * time.Hour).Format(time.RFC3339) + authOutput.SetExpiration(futureExpiration) jsonResponse, _ := json.Marshal(authOutput) w.WriteHeader(http.StatusOK) w.Write(jsonResponse) @@ -1128,6 +1174,7 @@ func TestGetBulkSecretValuesFromDifferentPaths(t *testing.T) { err := store.Init(context.Background(), meta) require.NoError(t, err) + defer store.Close() // Clean up background goroutine response, err := store.BulkGetSecret(context.Background(), secretstores.BulkGetSecretRequest{}) require.NoError(t, err) diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index f481ffe782..02a27c03b7 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -11,6 +11,7 @@ import ( "os" "regexp" "strings" + "time" "github.com/akeylesslabs/akeyless-go/v5" "github.com/dapr/components-contrib/secretstores" @@ -18,23 +19,24 @@ import ( ) const ( - 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" - STATIC_SECRET_TYPE = "static-secret" - DYNAMIC_SECRET_TYPE = "dynamic-secret" - ROTATED_SECRET_TYPE = "rotated-secret" - ALL_SECRET_TYPES = "all" - CLIENT_SOURCE = "akeylessclienttype" - PATH_DEFAULT = "/" - METADATA_PATH_KEY = "path" - METADATA_SECRETS_TYPE_KEY = "secrets_type" + 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" + STATIC_SECRET_TYPE = "static-secret" + DYNAMIC_SECRET_TYPE = "dynamic-secret" + ROTATED_SECRET_TYPE = "rotated-secret" + ALL_SECRET_TYPES = "all" + CLIENT_SOURCE = "akeylessclienttype" + PATH_DEFAULT = "/" + METADATA_PATH_KEY = "path" + METADATA_SECRETS_TYPE_KEY = "secrets_type" + TOKEN_REFRESH_GRACE_PERIOD = 5 * time.Minute ) var supportedSecretTypes = []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROTATED_SECRET_TYPE} @@ -301,3 +303,24 @@ func createTLSConfig(gatewayTLSCA string) (*tls.Config, error) { RootCAs: caCertPool, }, nil } + +func parseTokenExpirationDate(expirationStr string) (time.Time, error) { + // Try multiple formats to handle different expiration date formats + // Format 1: ISO 8601 format "2025-01-01T00:00:00Z" (used in tests) + layouts := []string{ + time.RFC3339, // "2006-01-02T15:04:05Z07:00" + time.RFC3339Nano, // "2006-01-02T15:04:05.999999999Z07:00" + "2006-01-02T15:04:05Z", // "2006-01-02T15:04:05Z" + "2006-01-02 15:04:05 -0700 MST", // "2025-12-09 21:35:00 +0000 UTC" (custom format) + "2006-01-02 15:04:05 -0700", // "2025-12-09 21:35:00 +0000" (without MST) + } + + for _, layout := range layouts { + parsedTime, err := time.Parse(layout, expirationStr) + if err == nil { + return parsedTime, nil + } + } + + return time.Time{}, fmt.Errorf("failed to parse token expiration date '%s' with any supported format", expirationStr) +} From bb842933e2a2bc7a0fe5f071491cacc4db419d58 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Fri, 5 Dec 2025 10:32:00 -0500 Subject: [PATCH 10/19] readd akeyless dependencies Signed-off-by: Kobbi Gal --- go.mod | 15 +++++++-- go.sum | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 8842dd879f..9db396932d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/dapr/components-contrib -go 1.24.4 +go 1.24.6 + +toolchain go1.24.10 require ( cloud.google.com/go/datastore v1.20.0 @@ -26,6 +28,8 @@ 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.16 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 @@ -58,13 +62,13 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 github.com/chebyrash/promise v0.0.0-20230709133807-42ec49ba1459 github.com/cinience/go_rocketmq v0.0.2 - github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0 + github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 github.com/cloudevents/sdk-go/v2 v2.15.2 github.com/cloudwego/kitex v0.5.0 github.com/cloudwego/kitex-examples v0.1.1 github.com/cyphar/filepath-securejoin v0.2.4 github.com/dancannon/gorethink v4.0.0+incompatible - github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 + github.com/dapr/kit v0.16.1 github.com/didip/tollbooth/v7 v7.0.1 github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/fasthttp-contrib/sessions v0.0.0-20160905201309-74f6ac73d5d5 @@ -153,11 +157,15 @@ require ( require ( cel.dev/expr v0.23.0 // indirect cloud.google.com/go v0.120.0 // indirect + cloud.google.com/go/ai v0.7.0 // indirect + cloud.google.com/go/aiplatform v1.86.0 // indirect cloud.google.com/go/auth v0.16.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/iam v1.5.2 // indirect + cloud.google.com/go/longrunning v0.6.7 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect + cloud.google.com/go/vertexai v0.12.0 // indirect contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect @@ -278,6 +286,7 @@ require ( github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect + github.com/google/generative-ai-go v0.15.1 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect diff --git a/go.sum b/go.sum index 4aace7746b..4c36de4767 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,17 @@ 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/ai v0.7.0 h1:P6+b5p4gXlza5E+u7uvcgYlzZ7103ACg70YdZeC6oGE= +cloud.google.com/go/ai v0.7.0/go.mod h1:7ozuEcraovh4ABsPbrec3o4LmFl9HigNI3D5haxYeQo= +cloud.google.com/go/aiplatform v1.86.0 h1:b8FVN8Jv4R0c1qMzqzURiJYXLp9R6Wx7d0q4MPGlTeM= +cloud.google.com/go/aiplatform v1.86.0/go.mod h1:xp3wFix8imliXkVpgMRkjnreJYTaNzLF44GOrnIENto= cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -61,6 +70,8 @@ cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6Q cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= +cloud.google.com/go/vertexai v0.12.0 h1:zTadEo/CtsoyRXNx3uGCncoWAP1H2HakGqwznt+iMo8= +cloud.google.com/go/vertexai v0.12.0/go.mod h1:8u+d0TsvBfAAd2x5R6GMgbYhsLgo3J7lmP4bR8g2ig8= contrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U= contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg= contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ= @@ -81,8 +92,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 +105,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 +137,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 +192,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.16 h1:nH0ExvPnfWMhHL3DovUQBXST/2Dj02KJxIHFYMqRauo= +github.com/akeylesslabs/akeyless-go/v5 v5.0.16/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 +292,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= @@ -421,8 +443,8 @@ github.com/clbanning/mxj/v2 v2.5.6 h1:Jm4VaCI/+Ug5Q57IzEoZbwx4iQFA6wkXv72juUSeK+ github.com/clbanning/mxj/v2 v2.5.6/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0 h1:dEopBSOSjB5fM9r76ufM44AVj9Dnz2IOM0Xs6FVxZRM= -github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.14.0/go.mod h1:qDSbb0fgIfFNjZrNTPtS5MOMScAGyQtn1KlSvoOdqYw= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2 h1:FIvfKlS2mcuP0qYY6yzdIU9xdrRd/YMP0bNwFjXd0u8= +github.com/cloudevents/sdk-go/binding/format/protobuf/v2 v2.15.2/go.mod h1:POsdVp/08Mki0WD9QvvgRRpg9CQ6zhjfRrBoEY8JFS8= github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc= github.com/cloudevents/sdk-go/v2 v2.15.2/go.mod h1:lL7kSWAE/V8VI4Wh0jbL2v/jvqsm6tjmaQBSvxcv4uE= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58 h1:F1EaeKL/ta07PY/k9Os/UFtwERei2/XzGemhpGnBKNg= @@ -466,6 +488,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= @@ -516,8 +539,8 @@ github.com/dancannon/gorethink v4.0.0+incompatible h1:KFV7Gha3AuqT+gr0B/eKvGhbjm github.com/dancannon/gorethink v4.0.0+incompatible/go.mod h1:BLvkat9KmZc1efyYwhz3WnybhRZtgF1K929FD8z1avU= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5 h1:Q26gmPxs6WnnBYoudOlznPHsmrbTawcYEpHg4VoB7v8= -github.com/dapr/kit v0.15.3-0.20250717140748-8b780b4d81c5/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= +github.com/dapr/kit v0.16.1 h1:MqLAhHVg8trPy2WJChMZFU7ToeondvxcNHYVvMDiVf4= +github.com/dapr/kit v0.16.1/go.mod h1:40ZWs5P6xfYf7O59XgwqZkIyDldTIXlhTQhGop8QoSM= github.com/dave/jennifer v1.4.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -548,6 +571,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 +620,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 +799,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 +841,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= @@ -839,6 +867,8 @@ github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76 github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/generative-ai-go v0.15.1 h1:n8aQUpvhPOlGVuM2DRkJ2jvx04zpp42B778AROJa+pQ= +github.com/google/generative-ai-go v0.15.1/go.mod h1:AAucpWZjXsDKhQYWvCYuP6d0yB1kX998pJlOW1rAesw= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -867,6 +897,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 +907,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 +922,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 +1324,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 +1465,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 +1828,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 +1951,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 +2007,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 +2060,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 +2093,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 +2103,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 +2187,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 +2269,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 +2280,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 +2365,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 +2417,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 +2432,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 +2468,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 +2510,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= From 21d3ae2130937bc3ae30ba3f4a18df69c8a7e615 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 10:42:01 -0500 Subject: [PATCH 11/19] implemented get component metadata in store Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 9c394489c2..d5cf1b752c 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/url" + "reflect" "strings" "sync" "time" @@ -14,6 +15,7 @@ import ( aws "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" "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" @@ -864,3 +866,9 @@ func (a *akeylessSecretStore) startTokenRefreshRoutine(ctx context.Context, meta } }() } + +func (a *akeylessSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) { + metadataStruct := akeylessMetadata{} + metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType) + return +} From 91b98718af0385febcc754eab96b9aaefd5b6dc9 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 11:58:41 -0500 Subject: [PATCH 12/19] camelcase gw tls ca field Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index d5cf1b752c..ab56224011 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -45,7 +45,7 @@ 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"` - GatewayTLSCA string `json:"gatewayTLSCA" mapstructure:"gatewayTLSCA"` + GatewayTlsCa string `json:"gatewayTlsCa" mapstructure:"gatewayTlsCa"` JWT string `json:"jwt" mapstructure:"jwt"` AccessID string `json:"accessId" mapstructure:"accessId"` AccessKey string `json:"accessKey" mapstructure:"accessKey"` @@ -141,10 +141,10 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle config.UserAgent = USER_AGENT config.AddDefaultHeader(CLIENT_SOURCE, USER_AGENT) - // Configure TLS if gatewayTLSCA is provided - if metadata.GatewayTLSCA != "" { + // Configure TLS if gatewayTlsCa is provided + if metadata.GatewayTlsCa != "" { a.logger.Debug("configuring TLS for Akeyless client...") - tlsConfig, err := createTLSConfig(metadata.GatewayTLSCA) + tlsConfig, err := createTLSConfig(metadata.GatewayTlsCa) if err != nil { return errors.New("failed to create TLS configuration: " + err.Error()) } From a63c83e9b6ee7f12065542ee6884c820763fb902 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 12:18:59 -0500 Subject: [PATCH 13/19] cont: camelcase gw tls ca field Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 4 ++-- secretstores/akeyless/metadata.yaml | 2 +- secretstores/akeyless/utils.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index e3bf13bdb0..ead73c1e69 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -18,7 +18,7 @@ The Akeyless secret store component supports the following configuration options | Field | Required | Description | Example | |-------|----------|-------------|---------| | `gatewayUrl` | No | The Akeyless Gateway API URL. Default is https://api.akeyless.io. | `https://gw.akeyless.svc.cluster.local:8000/api/v2` | -| `gatewayTLSCA` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. | `LS0tLS1CRUdJTi...` | +| `gatewayTlsCa` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. | `LS0tLS1CRUdJTi...` | | `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...=` | @@ -49,7 +49,7 @@ spec: metadata: - name: gatewayUrl value: "https://your-gateway.akeyless.io" - - name: gatewayTLSCA + - name: gatewayTlsCa value: "LS0tLS1CRUdJTi...." - name: accessId value: "p-1234Abcdam" diff --git a/secretstores/akeyless/metadata.yaml b/secretstores/akeyless/metadata.yaml index 098f5d1034..5063fec9fd 100644 --- a/secretstores/akeyless/metadata.yaml +++ b/secretstores/akeyless/metadata.yaml @@ -16,7 +16,7 @@ metadata: default: "https://api.akeyless.io" example: "https://your.akeyless.gw" type: string - - name: gatewayTLSCA + - name: gatewayTlsCa required: false description: | base64-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 02a27c03b7..5a8cd1a2f3 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -278,10 +278,10 @@ func parseSecretTypes(secretTypes string) ([]string, error) { return unique, nil } -func createTLSConfig(gatewayTLSCA string) (*tls.Config, error) { +func createTLSConfig(gatewayTlsCa string) (*tls.Config, error) { // Decode base64 to PEM - certBytes, err := base64.StdEncoding.DecodeString(gatewayTLSCA) + certBytes, err := base64.StdEncoding.DecodeString(gatewayTlsCa) if err != nil { return nil, fmt.Errorf("failed to decode base64-encoded gateway TLS CA: %w", err) } From ff08e2a0c46ff81ca78d7be70d1535aee9e7d4b0 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 12:23:40 -0500 Subject: [PATCH 14/19] cont: camelcase gw tls ca field Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 22 +++++++++++----------- secretstores/akeyless/akeyless_test.go | 6 +++--- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index ab56224011..6588d1887b 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -208,14 +208,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(ctx, req.Name) + secretType, err := a.getSecretType(ctx, req.Name) if err != nil { return secretstores.GetSecretResponse{}, errors.New("failed to get secret type: " + err.Error()) } a.logger.Debugf("getting secret value for '%s' (type %s)...", req.Name, secretType) - secretValue, err := a.GetSingleSecretValue(ctx, req.Name, secretType) + secretValue, err := a.getSingleSecretValue(ctx, req.Name, secretType) if err != nil { return secretstores.GetSecretResponse{}, errors.New(err.Error()) } @@ -307,14 +307,14 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore defer wg.Done() if len(staticItemNames) == 1 { staticSecretName := staticItemNames[0] - value, err := a.GetSingleSecretValue(ctx, staticSecretName, STATIC_SECRET_RESPONSE) + value, err := a.getSingleSecretValue(ctx, staticSecretName, STATIC_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: staticSecretName, value: "", err: err} } else { secretResultChannels <- secretResultCollection{name: staticSecretName, value: value, err: nil} } } else { - secretResponse := a.GetBulkStaticSecretValues(ctx, staticItemNames) + secretResponse := a.getBulkStaticSecretValues(ctx, staticItemNames) if len(secretResponse) > 0 { for _, result := range secretResponse { secretResultChannels <- result @@ -328,7 +328,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range dynamicItemNames { - value, err := a.GetSingleSecretValue(ctx, item, DYNAMIC_SECRET_RESPONSE) + value, err := a.getSingleSecretValue(ctx, item, DYNAMIC_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -342,7 +342,7 @@ func (a *akeylessSecretStore) BulkGetSecret(ctx context.Context, req secretstore go func() { defer wg.Done() for _, item := range rotatedItemNames { - value, err := a.GetSingleSecretValue(ctx, item, ROTATED_SECRET_RESPONSE) + value, err := a.getSingleSecretValue(ctx, item, ROTATED_SECRET_RESPONSE) if err != nil { secretResultChannels <- secretResultCollection{name: item, value: "", err: err} } else { @@ -423,7 +423,7 @@ func (a *akeylessSecretStore) parseMetadata(meta secretstores.Metadata) (*akeyle return &m, nil } -func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName string) (string, error) { +func (a *akeylessSecretStore) getSecretType(ctx context.Context, secretName string) (string, error) { if err := a.ensureValidToken(ctx); err != nil { return "", fmt.Errorf("failed to ensure valid token: %w", err) @@ -463,9 +463,9 @@ func (a *akeylessSecretStore) GetSecretType(ctx context.Context, secretName stri return *describeItemResp.ItemType, nil } -// GetSingleSecretValue gets the value of a single secret from Akeyless. +// 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(ctx context.Context, secretName string, secretType string) (string, error) { +func (a *akeylessSecretStore) getSingleSecretValue(ctx context.Context, secretName string, secretType string) (string, error) { if err := a.ensureValidToken(ctx); err != nil { return "", fmt.Errorf("failed to ensure valid token: %w", err) @@ -621,9 +621,9 @@ func (a *akeylessSecretStore) GetSingleSecretValue(ctx context.Context, secretNa return secretValue, err } -// GetBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. +// getBulkStaticSecretValues gets the values of multiple static secrets from Akeyless. // It returns a map of secret names and their values. -func (a *akeylessSecretStore) GetBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { +func (a *akeylessSecretStore) getBulkStaticSecretValues(ctx context.Context, secretNames []string) []secretResultCollection { if err := a.ensureValidToken(ctx); err != nil { return []secretResultCollection{ {name: "", value: "", err: fmt.Errorf("failed to ensure valid token: %w", err)}, diff --git a/secretstores/akeyless/akeyless_test.go b/secretstores/akeyless/akeyless_test.go index 8df88545ab..9f6d3eb273 100644 --- a/secretstores/akeyless/akeyless_test.go +++ b/secretstores/akeyless/akeyless_test.go @@ -693,7 +693,7 @@ func TestGetSecretType(t *testing.T) { require.NoError(t, err) defer store.Close() // Clean up background goroutine - secretType, err := store.GetSecretType(ctx, mockDescribeStaticSecretName) + secretType, err := store.getSecretType(ctx, mockDescribeStaticSecretName) assert.NoError(t, err) assert.Equal(t, STATIC_SECRET_RESPONSE, secretType) } @@ -747,7 +747,7 @@ func TestGetSingleDynamicSecret(t *testing.T) { require.NoError(t, err) defer store.Close() // Clean up background goroutine - secretValue, err := store.GetSingleSecretValue(ctx, 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() @@ -800,7 +800,7 @@ func TestGetSingleRotatedSecret(t *testing.T) { require.NoError(t, err) defer store.Close() // Clean up background goroutine - secretValue, err := store.GetSingleSecretValue(ctx, 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 e7ddc84a49ed3bb26d13f990032caaf3dc28b668 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 12:37:01 -0500 Subject: [PATCH 15/19] change default auth method Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 4 ++-- secretstores/akeyless/utils.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 6588d1887b..ccb3f0a783 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -97,14 +97,14 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle a.logger.Debugf("getting access type display name for character '%s'...", accessTypeChar) accessType, err := getAccessTypeDisplayName(accessTypeChar) if err != nil { - return errors.New("unable to get access type display name, expected format is p-([A-Za-z0-9]{14}|[A-Za-z0-9]{12})") + return errors.New("unable to get access type from character '" + accessTypeChar + "': " + err.Error()) } a.logger.Debugf("authenticating using access type '%s'", accessType) // Depending on the access type we set the appropriate authentication method switch accessType { - case DEFAULT_AUTH_TYPE: + case AUTH_DEFAULT: if metadata.AccessKey == "" { return errors.New("accessKey is required for API key authentication") } diff --git a/secretstores/akeyless/utils.go b/secretstores/akeyless/utils.go index 5a8cd1a2f3..f4f252bf49 100644 --- a/secretstores/akeyless/utils.go +++ b/secretstores/akeyless/utils.go @@ -20,7 +20,7 @@ import ( const ( AUTH_JWT = "jwt" - DEFAULT_AUTH_TYPE = "access_key" + AUTH_DEFAULT = "access_key" AUTH_IAM = "aws_iam" AUTH_K8S = "k8s" PUBLIC_GATEWAY_URL = "https://api.akeyless.io" @@ -43,7 +43,7 @@ var supportedSecretTypes = []string{STATIC_SECRET_TYPE, DYNAMIC_SECRET_TYPE, ROT // AccessTypeCharMap maps single-character access types to their display names. var accessTypeCharMap = map[string]string{ - "a": DEFAULT_AUTH_TYPE, + "a": AUTH_DEFAULT, "o": AUTH_JWT, "w": AUTH_IAM, "k": AUTH_K8S, From 45d562c67480ab4d029805b35b8d06eda1e58a6e Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 12:39:47 -0500 Subject: [PATCH 16/19] rm whitespace in go codeblock doc Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index ead73c1e69..02ffa3deaa 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -136,29 +136,29 @@ curl http://localhost:3500/v1.0/secrets/akeyless/bulk?metadata.secrets_type=stat Or using the Dapr SDK. The example below retrieves all static secrets from path `/path/to/department`. ```go log.Println("Starting test application") - client, err := dapr.NewClient() - if err != nil { - log.Printf("Error creating Dapr client: %v\n", err) - panic(err) - } - log.Println("Dapr client created successfully") - const daprSecretStore = "akeyless" - - defer client.Close() - ctx := context.Background() - akeylessBulkMetadata := map[string]string{ - "path": "/path/to/department", - "secrets_type": "static", - } - secrets, err := client.GetBulkSecret(ctx, daprSecretStore, akeylessBulkMetadata) - if err != nil { - log.Printf("Error fetching secrets: %v\n", err) - panic(err) - } - log.Printf("Found %d secrets: ", len(secrets)) - for secretName, secretValue := range secrets { - log.Printf("Secret: %s, Value: %s", secretName, secretValue) - } +client, err := dapr.NewClient() +if err != nil { + log.Printf("Error creating Dapr client: %v\n", err) + panic(err) +} +log.Println("Dapr client created successfully") +const daprSecretStore = "akeyless" + +defer client.Close() +ctx := context.Background() +akeylessBulkMetadata := map[string]string{ + "path": "/path/to/department", + "secrets_type": "static", +} +secrets, err := client.GetBulkSecret(ctx, daprSecretStore, akeylessBulkMetadata) +if err != nil { + log.Printf("Error fetching secrets: %v\n", err) + panic(err) +} +log.Printf("Found %d secrets: ", len(secrets)) +for secretName, secretValue := range secrets { + log.Printf("Secret: %s, Value: %s", secretName, secretValue) +} ``` ## Features From 41db6f7396f3dd5ae38c5a0913ebbbcdbcc8d393 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 12:46:57 -0500 Subject: [PATCH 17/19] added 30s timeout to http client when using tls Signed-off-by: Kobbi Gal --- secretstores/akeyless/README.md | 2 +- secretstores/akeyless/akeyless.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/secretstores/akeyless/README.md b/secretstores/akeyless/README.md index 02ffa3deaa..9ab86acabb 100644 --- a/secretstores/akeyless/README.md +++ b/secretstores/akeyless/README.md @@ -18,7 +18,7 @@ The Akeyless secret store component supports the following configuration options | Field | Required | Description | Example | |-------|----------|-------------|---------| | `gatewayUrl` | No | The Akeyless Gateway API URL. Default is https://api.akeyless.io. | `https://gw.akeyless.svc.cluster.local:8000/api/v2` | -| `gatewayTlsCa` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. | `LS0tLS1CRUdJTi...` | +| `gatewayTlsCa` | No | The `base64`-encoded PEM certificate of the Akeyless Gateway. Use this when connecting to a gateway with a self-signed or custom CA certificate. The Akeyless client will be set to a 30 second timeout. | `LS0tLS1CRUdJTi...` | | `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...=` | diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index ccb3f0a783..2dafac03d0 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -150,6 +150,7 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle } httpClient := &http.Client{ + Timeout: 30 * time.Second, Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, From c2e1b1c43e759c7e0f570bc12e521c04c92dcccc Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 12:49:58 -0500 Subject: [PATCH 18/19] check auth response output for token/expiration Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index 2dafac03d0..a04f239dbc 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -177,6 +177,12 @@ func (a *akeylessSecretStore) authenticate(ctx context.Context, metadata *akeyle } return fmt.Errorf("failed to authenticate with Akeyless (HTTP status code: %d): %s", statusCode, status) } + if out != nil && out.GetToken() == "" { + return errors.New("authentication failed, no token returned") + } + if out != nil && out.GetExpiration() == "" { + return errors.New("authentication failed, no expiration time returned") + } a.logger.Debugf("authentication successful - token expires at %s", out.GetExpiration()) From 2598ccf826c6c2438e0b39986275d59c7171ed31 Mon Sep 17 00:00:00 2001 From: Kobbi Gal Date: Wed, 10 Dec 2025 13:29:49 -0500 Subject: [PATCH 19/19] refactor to use helper for 401 retry Signed-off-by: Kobbi Gal --- secretstores/akeyless/akeyless.go | 261 +++++++++++++++++++----------- 1 file changed, 169 insertions(+), 92 deletions(-) diff --git a/secretstores/akeyless/akeyless.go b/secretstores/akeyless/akeyless.go index a04f239dbc..587a3c782b 100644 --- a/secretstores/akeyless/akeyless.go +++ b/secretstores/akeyless/akeyless.go @@ -443,26 +443,25 @@ func (a *akeylessSecretStore) getSecretType(ctx context.Context, secretName stri a.mu.RUnlock() describeItem.SetToken(token) - describeItemResp, httpResponse, err := a.v2.DescribeItem(ctx).Body(*describeItem).Execute() - if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { - a.logger.Debug("received 401 unauthorized, re-authenticating...") - if err := a.ensureValidToken(ctx); err != nil { - return "", fmt.Errorf("failed to re-authenticate after 401: %w", err) - } - - a.mu.RLock() - token = a.token - a.mu.RUnlock() - - describeItem.SetToken(token) - describeItemResp, _, err = a.v2.DescribeItem(ctx).Body(*describeItem).Execute() - } + result, _, err := a.executeWithRetryOn401( + ctx, + "DescribeItem", + describeItem, + func(newToken string) { + describeItem.SetToken(newToken) + }, + ) if err != nil { return "", fmt.Errorf("failed to describe item '%s': %w", secretName, err) } + describeItemResp, ok := result.(*akeyless.Item) + if !ok { + return "", fmt.Errorf("unexpected result type from DescribeItem: %T", result) + } + if describeItemResp.ItemType == nil { return "", errors.New("unable to retrieve secret type, missing type in describe item response") } @@ -470,6 +469,107 @@ func (a *akeylessSecretStore) getSecretType(ctx context.Context, secretName stri return *describeItemResp.ItemType, nil } +// executeWithRetryOn401 executes an API call using reflection and retries once if it receives a 401 Unauthorized response. +// It takes the method name (e.g., "GetSecretValue"), the body object, and a function to update the token in the body. +// Returns the result, httpResponse, and error using reflection. +func (a *akeylessSecretStore) executeWithRetryOn401( + ctx context.Context, + methodName string, + body interface{}, + updateToken func(string), +) (interface{}, *http.Response, error) { + // Helper to get current token (with mutex protection) + getToken := func() string { + a.mu.RLock() + defer a.mu.RUnlock() + return a.token + } + + // Helper function to execute the API call using reflection + executeCall := func() (interface{}, *http.Response, error) { + // Use reflection to call the method dynamically + v2Value := reflect.ValueOf(a.v2) + method := v2Value.MethodByName(methodName) + if !method.IsValid() { + return nil, nil, fmt.Errorf("method %s not found on V2ApiService", methodName) + } + + // Call the method with context: a.v2.MethodName(ctx) + ctxValue := reflect.ValueOf(ctx) + callResult := method.Call([]reflect.Value{ctxValue}) + if len(callResult) == 0 { + return nil, nil, fmt.Errorf("method %s returned no values", methodName) + } + + // Get the Body() method from the result: result.Body() + bodyMethod := callResult[0].MethodByName("Body") + if !bodyMethod.IsValid() { + return nil, nil, fmt.Errorf("Body method not found on result of %s", methodName) + } + + // Call Body(*body): result.Body(*body) + // Body() expects a value (not a pointer), so we need to dereference if it's a pointer + bodyValue := reflect.ValueOf(body) + if bodyValue.Kind() == reflect.Ptr { + // Dereference the pointer to get the value + bodyValue = bodyValue.Elem() + } + // Pass the value to Body() + bodyCallResult := bodyMethod.Call([]reflect.Value{bodyValue}) + if len(bodyCallResult) == 0 { + return nil, nil, fmt.Errorf("Body method returned no values") + } + + // Get the Execute() method: result.Body(*body).Execute() + executeMethod := bodyCallResult[0].MethodByName("Execute") + if !executeMethod.IsValid() { + return nil, nil, fmt.Errorf("Execute method not found on Body result") + } + + // Execute the API call: result.Body(*body).Execute() + executeResult := executeMethod.Call([]reflect.Value{}) + if len(executeResult) < 3 { + return nil, nil, fmt.Errorf("Execute method did not return 3 values (result, response, error)") + } + + // Extract results + var result interface{} + var httpResponse *http.Response + var apiErr error + + if !executeResult[0].IsNil() { + result = executeResult[0].Interface() + } + if !executeResult[1].IsNil() { + httpResponse = executeResult[1].Interface().(*http.Response) + } + if !executeResult[2].IsNil() { + apiErr = executeResult[2].Interface().(error) + } + + return result, httpResponse, apiErr + } + + // Execute the API call + result, httpResponse, apiErr := executeCall() + + // Check for 401 Unauthorized using the actual HTTP status code + if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { + a.logger.Debugf("received 401 unauthorized in %s, re-authenticating...", methodName) + if reauthErr := a.ensureValidToken(ctx); reauthErr != nil { + return nil, httpResponse, fmt.Errorf("failed to re-authenticate after 401: %w", reauthErr) + } + // Update token in the request object before retry + newToken := getToken() + updateToken(newToken) + + // Retry the API call once + return executeCall() + } + + return result, httpResponse, apiErr +} + // 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(ctx context.Context, secretName string, secretType string) (string, error) { @@ -485,54 +585,31 @@ func (a *akeylessSecretStore) getSingleSecretValue(ctx context.Context, secretNa token := a.token a.mu.RUnlock() - // Helper to get current token (with mutex protection) - getToken := func() string { - a.mu.RLock() - defer a.mu.RUnlock() - return a.token - } - - retry := func(apiCall func() error, updateToken func(string)) error { - apiErr := apiCall() - if apiErr != nil { - // Check if it's a 401 error by examining the error string or response - if strings.Contains(apiErr.Error(), "401") || strings.Contains(apiErr.Error(), "Unauthorized") { - a.logger.Debug("received 401 unauthorized, re-authenticating...") - if reauthErr := a.ensureValidToken(ctx); reauthErr != nil { - return fmt.Errorf("failed to re-authenticate after 401: %w", reauthErr) - } - // Update token in the request object before retry - newToken := getToken() - updateToken(newToken) - return apiCall() - } - } - return apiErr - } - switch secretType { case STATIC_SECRET_RESPONSE: getSecretValue := akeyless.NewGetSecretValue([]string{secretName}) getSecretValue.SetToken(token) - var secretRespMap map[string]interface{} - var httpResponse *http.Response - apiErr := retry(func() error { - var err error - secretRespMap, httpResponse, err = a.v2.GetSecretValue(ctx).Body(*getSecretValue).Execute() - if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { - return fmt.Errorf("401 Unauthorized") - } - return err - }, func(newToken string) { - getSecretValue.SetToken(newToken) - }) + result, _, apiErr := a.executeWithRetryOn401( + ctx, + "GetSecretValue", + getSecretValue, + func(newToken string) { + getSecretValue.SetToken(newToken) + }, + ) if apiErr != nil { err = fmt.Errorf("failed to get secret '%s' value for static secret from Akeyless API: %w", secretName, apiErr) break } + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetSecretValue: %T", result) + break + } + // check if secret key is in response value, ok := secretRespMap[secretName] if !ok { @@ -551,25 +628,27 @@ func (a *akeylessSecretStore) getSingleSecretValue(ctx context.Context, secretNa case DYNAMIC_SECRET_RESPONSE: getDynamicSecretValue := akeyless.NewGetDynamicSecretValue(secretName) getDynamicSecretValue.SetToken(token) - var secretRespMap map[string]interface{} - var httpResponse *http.Response - apiErr := retry(func() error { - var err error - secretRespMap, httpResponse, err = a.v2.GetDynamicSecretValue(ctx).Body(*getDynamicSecretValue).Execute() - if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { - return fmt.Errorf("401 Unauthorized") - } - return err - }, func(newToken string) { - getDynamicSecretValue.SetToken(newToken) - }) + result, _, apiErr := a.executeWithRetryOn401( + ctx, + "GetDynamicSecretValue", + getDynamicSecretValue, + func(newToken string) { + getDynamicSecretValue.SetToken(newToken) + }, + ) if apiErr != nil { err = fmt.Errorf("failed to get dynamic secret '%s' value from Akeyless API: %w", secretName, apiErr) break } + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetDynamicSecretValue: %T", result) + break + } + // Parse response to extract value and check for errors var dynamicSecretResp struct { Value string `json:"value"` @@ -597,25 +676,27 @@ func (a *akeylessSecretStore) getSingleSecretValue(ctx context.Context, secretNa case ROTATED_SECRET_RESPONSE: getRotatedSecretValue := akeyless.NewGetRotatedSecretValue(secretName) getRotatedSecretValue.SetToken(token) - var secretRespMap map[string]interface{} - var httpResponse *http.Response - apiErr := retry(func() error { - var err error - secretRespMap, httpResponse, err = a.v2.GetRotatedSecretValue(ctx).Body(*getRotatedSecretValue).Execute() - if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { - return fmt.Errorf("401 Unauthorized") - } - return err - }, func(newToken string) { - getRotatedSecretValue.SetToken(newToken) - }) + result, _, apiErr := a.executeWithRetryOn401( + ctx, + "GetRotatedSecretValue", + getRotatedSecretValue, + func(newToken string) { + getRotatedSecretValue.SetToken(newToken) + }, + ) if apiErr != nil { err = fmt.Errorf("failed to get rotated secret '%s' value from Akeyless API: %w", secretName, apiErr) break } + secretRespMap, ok := result.(map[string]interface{}) + if !ok { + err = fmt.Errorf("unexpected result type from GetRotatedSecretValue: %T", result) + break + } + // Marshal the entire response value object jsonBytes, marshalErr := json.Marshal(secretRespMap) if marshalErr != nil { @@ -704,28 +785,24 @@ func (a *akeylessSecretStore) listItemsRecursively(ctx context.Context, path str // Execute the list items request a.logger.Debugf("listing items from path '%s'...", path) - itemsList, httpResponse, err := a.v2.ListItems(ctx).Body(*listItems).Execute() - - // Handle 401 Unauthorized by re-authenticating and retrying once - // Check this BEFORE checking err, as 401 might come with an error - if httpResponse != nil && httpResponse.StatusCode == http.StatusUnauthorized { - a.logger.Debug("received 401 Unauthorized in list items, re-authenticating...") - if err := a.ensureValidToken(ctx); err != nil { - return nil, fmt.Errorf("failed to re-authenticate after 401: %w", err) - } - - a.mu.RLock() - token = a.token - a.mu.RUnlock() - - listItems.SetToken(token) - itemsList, _, err = a.v2.ListItems(ctx).Body(*listItems).Execute() - } + result, _, err := a.executeWithRetryOn401( + ctx, + "ListItems", + listItems, + func(newToken string) { + listItems.SetToken(newToken) + }, + ) if err != nil { return nil, err } + itemsList, ok := result.(*akeyless.ListItemsInPathOutput) + if !ok { + return nil, fmt.Errorf("unexpected result type from ListItems: %T", result) + } + // Add items from current path if itemsList.Items != nil { allItems = append(allItems, itemsList.Items...)