Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions kv/internal/resolve/refs.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ type refKey struct {
fragment string
}

// ParseWholeValue is the exported boundary over parseWholeValue for callers
// outside the resolve engine that must parse a kv:// reference without resolving it.
func ParseWholeValue(input string) (store, path, fragment string, ok bool, err error) {
rk, ok, err := parseWholeValue(input)
return rk.store, rk.path, rk.fragment, ok, err
}

// parseWholeValue parses a whole-value reference of the form
// "kv://store/path#frag".
//
Expand Down
4 changes: 1 addition & 3 deletions kv/internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
"golang.org/x/sync/singleflight"
)

const defaultProviderTimeout = 5 * time.Second

// SecretStore is an internal decorator that adds caching and singleflight to a Provider.
type SecretStore struct {
name string
Expand Down Expand Up @@ -169,7 +167,7 @@ func NewSecretStore(
cache: cache,
sf: &singleflight.Group{},
sfRefresh: &singleflight.Group{},
timeout: defaultProviderTimeout,
timeout: kv.DefaultOperationTimeout,
}

for _, opt := range opts {
Expand Down
2 changes: 1 addition & 1 deletion kv/internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
})
require.NoError(t, err)
require.NotNil(t, store)
require.Equal(t, defaultProviderTimeout, store.timeout)
require.Equal(t, kv.DefaultOperationTimeout, store.timeout)
})

t.Run("cache disabled", func(t *testing.T) {
Expand Down Expand Up @@ -671,7 +671,7 @@
// BenchmarkSecretStoreGet measures the per-request cost of a Get through the
// SecretStore wrapper — the "no measurable per-request latency when values are
// cache-resident" acceptance criterion.
func BenchmarkSecretStoreGet(b *testing.B) {

Check failure on line 674 in kv/internal/store/store_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=TykTechnologies_storage&issues=AZ9wjF3NMh4lWgnPSwAM&open=AZ9wjF3NMh4lWgnPSwAM&pullRequest=155
ctx := context.Background()

b.Run("cache-hit", func(b *testing.B) {
Expand Down
30 changes: 28 additions & 2 deletions kv/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
Conjur ProviderType = "cyberark_conjur"
)

// DefaultOperationTimeout bounds a single provider Get/Set when neither the store
// config nor the SecretStore wrapper supplies one.
const DefaultOperationTimeout = 5 * time.Second

// IsLocal reports whether this provider type resolves secrets from resources
// available to the local process — environment variables, inline config data,
// or the filesystem — requiring no network and a literal, reference-free config.
Expand Down Expand Up @@ -101,11 +105,17 @@
Init(ctx context.Context) error
}

// Setter is an optional interface for providers that support writing values
// back to their backend.
type Setter interface {
Set(ctx context.Context, key, value string) error
}

// Lister is an optional interface for providers that support enumerating
// keys by prefix. This enables dynamic discovery of available secrets
// keys & values by prefix. This enables dynamic discovery of available secrets
// and operational tooling.
type Lister interface {
List(ctx context.Context, prefix string) ([]string, error)
List(ctx context.Context, prefix string) (map[string]string, error)
}

// Closer is an optional interface for providers that need graceful shutdown
Expand All @@ -116,7 +126,7 @@

// Standalone is an optional interface for providers that do not need
// to be combined with caching or singleflight mechanisms.
type Standalone interface {

Check warning on line 129 in kv/provider.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename this interface to follow Go naming conventions for single-method interfaces.

See more on https://sonarcloud.io/project/issues?id=TykTechnologies_storage&issues=AZ9wjF4NMh4lWgnPSwAO&open=AZ9wjF4NMh4lWgnPSwAO&pullRequest=155
IsStandalone() bool
}

Expand All @@ -126,6 +136,12 @@
Timeout() time.Duration
}

// AsSetter attempts to extract a Setter from a Provider,
// automatically unwrapping decorators.
func AsSetter(p Provider) (Setter, bool) {
return As[Setter](p)
}

// AsLister attempts to extract a Lister from a Provider,
// automatically unwrapping decorators.
func AsLister(p Provider) (Lister, bool) {
Expand Down Expand Up @@ -179,3 +195,13 @@

return zero, false
}

// EffectiveTimeout resolves a configured timeout to the value actually used:
// the configured value when positive, else the default.
func EffectiveTimeout(configured time.Duration) time.Duration {
if configured > 0 {
return configured
}

return DefaultOperationTimeout
}
69 changes: 62 additions & 7 deletions kv/providers/consul/consul.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/TykTechnologies/storage/kv"
"github.com/hashicorp/consul/api"
consulsdk "github.com/hashicorp/consul/api"
)

// Config is the JSON "config" block of a consul store.
Expand Down Expand Up @@ -62,7 +64,7 @@
// JSON, an unparseable wait_time, or TLS settings the client rejects (e.g. an
// unreadable ca_file). Absent config is valid — an empty blob builds a client
// against consul's default local agent.
func NewFactory() kv.ProviderFactory {

Check failure on line 67 in kv/providers/consul/consul.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=TykTechnologies_storage&issues=AZ9wjF0aMh4lWgnPSwAK&open=AZ9wjF0aMh4lWgnPSwAK&pullRequest=155
return func(rawJSON json.RawMessage) (kv.Provider, error) {
var conf Config

Expand All @@ -74,7 +76,7 @@
}
}

clientCfg := api.DefaultConfig()
clientCfg := consulsdk.DefaultConfig()

if conf.Address != "" {
clientCfg.Address = conf.Address
Expand All @@ -89,7 +91,7 @@
}

if conf.HttpAuth.Username != "" || conf.HttpAuth.Password != "" {
clientCfg.HttpAuth = &api.HttpBasicAuth{
clientCfg.HttpAuth = &consulsdk.HttpBasicAuth{
Username: conf.HttpAuth.Username,
Password: conf.HttpAuth.Password,
}
Expand All @@ -112,7 +114,7 @@

applyTLSConfig(clientCfg, &conf)

client, err := api.NewClient(clientCfg)
client, err := consulsdk.NewClient(clientCfg)
if err != nil {
return nil, fmt.Errorf("consul: create client: %w", err)
}
Expand All @@ -121,7 +123,7 @@
}
}

func applyTLSConfig(clientCfg *api.Config, conf *Config) {
func applyTLSConfig(clientCfg *consulsdk.Config, conf *Config) {
tls := conf.TLSConfig

if tls.Address != "" {
Expand Down Expand Up @@ -153,7 +155,7 @@
type consulProvider struct {
// kvClient is consul's KV endpoint, with the resolved Config already baked
// into the underlying client.
kvClient *api.KV
kvClient *consulsdk.KV
}

// Get reads the value at key and returns it verbatim: no trimming, no key
Expand All @@ -164,7 +166,7 @@
// distinguishes. ctx bounds the request via QueryOptions, so the SecretStore's
// per-operation deadline is honored.
func (cp *consulProvider) Get(ctx context.Context, key string) (string, error) {
pair, _, err := cp.kvClient.Get(key, (&api.QueryOptions{}).WithContext(ctx))
pair, _, err := cp.kvClient.Get(key, (&consulsdk.QueryOptions{}).WithContext(ctx))
if err != nil {
return "", &kv.StoreUnavailableError{KeyPath: key, Err: err}
}
Expand All @@ -175,3 +177,56 @@

return string(pair.Value), nil
}

// Set writes value verbatim as the raw bytes at key (PUT /v1/kv/<key>), with no
// key transformation or interpretation.
// A transport or backend failure returns *kv.StoreUnavailableError.
func (cp *consulProvider) Set(ctx context.Context, key, value string) error {
pair := &consulsdk.KVPair{
Key: key,
Value: []byte(value),
}

ctx, cancel := context.WithTimeout(ctx, kv.DefaultOperationTimeout)
defer cancel()

_, err := cp.kvClient.Put(pair, (&consulsdk.WriteOptions{}).WithContext(ctx))
if err != nil {
return &kv.StoreUnavailableError{KeyPath: key, Err: err}
}

return nil
}

// List returns every key/value pair under prefix, keyed by the FULL consul key
// (the caller strips the prefix if it wants relative keys). Consul directory

Check warning on line 202 in kv/providers/consul/consul.go

View check run for this annotation

probelabs / Visor: security

architecture Issue

The Set operation uses a hardcoded default timeout (`kv.DefaultOperationTimeout`) instead of a configurable timeout. This can lead to inflexibility in environments with different network latencies or operational requirements, and may result in premature timeouts for slow Consul operations.
Raw output
The `consulProvider` should have a `timeout` field, similar to the `vaultProvider`, which is initialized from the configuration. The `Set` method should then use this configurable timeout. This allows operators to tune performance and reliability according to their specific environment.
// markers — keys ending in "/" — are skipped; they are not real entries.
//
// An empty prefix is rejected: consul would treat it as "list the entire KV
// store", which is never what a reference resolver wants and is an easy footgun.
// A prefix that matches nothing is not an error — it returns an empty map.
func (cp *consulProvider) List(ctx context.Context, prefix string) (map[string]string, error) {
if prefix == "" {
return nil, errors.New("consul: list requires a non-empty prefix")

Check warning on line 210 in kv/providers/consul/consul.go

View check run for this annotation

probelabs / Visor: quality

architecture Issue

The `Set` and `List` methods in the Consul provider use a hardcoded default timeout (`kv.DefaultOperationTimeout`), whereas the `Set` method in the Vault provider uses a configurable timeout. This creates an inconsistency where users can configure timeouts for Vault write operations but not for Consul write or list operations.
Raw output
To ensure consistent behavior across providers, add a `Timeout` field to `consul.Config`, similar to `vault.Config`. Then, use `kv.EffectiveTimeout` in the `Set` and `List` methods to apply the configured timeout or the default. This will provide a more consistent and predictable experience for users configuring different types of secret stores.
}

ctx, cancel := context.WithTimeout(ctx, kv.DefaultOperationTimeout)
defer cancel()

Check warning on line 214 in kv/providers/consul/consul.go

View check run for this annotation

probelabs / Visor: performance

performance Issue

The `Set` and `List` methods in the Consul provider use a hardcoded default timeout (`kv.DefaultOperationTimeout`). This is inconsistent with the Vault provider, which uses a configurable timeout with a default fallback. This lack of configurability can be problematic in environments with different network latencies or performance requirements, as it prevents tuning the timeout for Consul-specific operations.
Raw output
To provide consistent behavior and better control over operation deadlines across providers, add a `Timeout` field to the `consul.Config` struct. Then, use `kv.EffectiveTimeout` to apply the configured timeout, falling back to the default if not set. This would align the Consul provider's behavior with the Vault provider's.

Check warning on line 215 in kv/providers/consul/consul.go

View check run for this annotation

probelabs / Visor: architecture

architecture Issue

The Consul provider uses a hardcoded default timeout for `Set` and `List` operations, which is inconsistent with the Vault provider that supports a configurable timeout. This limits the flexibility of the Consul provider and creates a divergence in behavior and configuration options between providers.
Raw output
To ensure consistent configuration and behavior across all providers, the Consul provider should be updated to support a configurable timeout. This involves adding a `Timeout` field to the `consul.Config` struct, storing it in the `consulProvider`, and using the `kv.EffectiveTimeout` helper function when creating contexts for `Set` and `List` operations, mirroring the implementation in the Vault provider.
pairs, _, err := cp.kvClient.List(prefix, (&consulsdk.QueryOptions{}).WithContext(ctx))
if err != nil {

Check warning on line 217 in kv/providers/consul/consul.go

View check run for this annotation

probelabs / Visor: performance

performance Issue

The `List` method for the Consul provider fetches all key-value pairs under a given prefix and loads them into memory at once to construct the returned map. If a prefix matches a very large number of keys, this can lead to excessive memory allocation, potentially causing performance degradation or Out-of-Memory (OOM) errors. While the check for an empty prefix is a good safeguard, a broad, non-empty prefix could still match a huge dataset, posing a resource exhaustion risk.
Raw output
Given the `Lister` interface contract returns a `map`, in-memory collection is required. Add documentation to the `List` method to explicitly warn users about the potential memory impact of using broad prefixes. For a more robust solution, consider logging a warning if the number of returned items exceeds a high threshold (e.g., 10,000) to help diagnose potential performance issues in production.
return nil, &kv.StoreUnavailableError{KeyPath: prefix, Err: err}
}

out := make(map[string]string, len(pairs))

for _, p := range pairs {
if strings.HasSuffix(p.Key, "/") {

Check warning on line 224 in kv/providers/consul/consul.go

View check run for this annotation

probelabs / Visor: security

architecture Issue

The List operation uses a hardcoded default timeout (`kv.DefaultOperationTimeout`) instead of a configurable timeout. Listing many keys could be a slow operation, and a hardcoded timeout may not be sufficient in all use cases, potentially causing requests to fail unnecessarily.
Raw output
The `consulProvider` should have a `timeout` field, similar to the `vaultProvider`, which is initialized from the configuration. The `List` method should then use this configurable timeout, providing flexibility for different operational scenarios.
continue
}

out[p.Key] = string(p.Value)
}

return out, nil
}
Loading
Loading