Skip to content
Merged
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
2 changes: 2 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1"
"github.com/hashicorp/vault-secrets-operator/consts"
vaultcredsconsts "github.com/hashicorp/vault-secrets-operator/credentials/vault/consts"
"github.com/hashicorp/vault-secrets-operator/internal/version"
"github.com/hashicorp/vault-secrets-operator/utils"
)

Expand All @@ -30,6 +31,7 @@ var (
InvalidObjectKeyError = fmt.Errorf("invalid objectKey")
InvalidObjectKeyErrorEmptyName = fmt.Errorf("%w, empty name", InvalidObjectKeyError)
InvalidObjectKeyErrorEmptyNamespace = fmt.Errorf("%w, empty namespace", InvalidObjectKeyError)
DefaultVSOUserAgent = fmt.Sprintf("vso/%s", version.Version().String())
defaultMaxRetries uint64 = 60
defaultRetryDuration = time.Millisecond * 500
)
Expand Down
1 change: 1 addition & 0 deletions consts/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ const (
AWSSessionToken = "session_token"

AnnotationResync = "vso.hashicorp.com/resync"
HeaderUserAgent = "User-Agent"
)
7 changes: 2 additions & 5 deletions controllers/hcpvaultsecretsapp_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,10 @@ import (
"github.com/hashicorp/vault-secrets-operator/common"
"github.com/hashicorp/vault-secrets-operator/consts"
"github.com/hashicorp/vault-secrets-operator/helpers"
"github.com/hashicorp/vault-secrets-operator/internal/version"
)

const (
headerHVSRequester = "X-HVS-Requester"
headerUserAgent = "User-Agent"

hcpVaultSecretsAppFinalizer = "hcpvaultsecretsapp.secrets.hashicorp.com/finalizer"

Expand All @@ -67,7 +65,6 @@ const (
)

var (
userAgent = fmt.Sprintf("vso/%s", version.Version().String())
// hvsErrorRe is a regexp to parse the error message from the HVS API
// The error message is expected to be in the format:
// [METHOD PATH_PATTERN][STATUS_CODE]
Expand Down Expand Up @@ -402,8 +399,8 @@ type transport struct {
// RoundTrip is a wrapper implementation of the http.RoundTrip interface to
// inject a header for identifying the requester type
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Add(headerUserAgent, userAgent)
req.Header.Add(headerHVSRequester, userAgent)
req.Header.Add(consts.HeaderUserAgent, common.DefaultVSOUserAgent)
req.Header.Add(headerHVSRequester, common.DefaultVSOUserAgent)
return t.child.RoundTrip(req)
}

Expand Down
21 changes: 20 additions & 1 deletion vault/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type ClientOptions struct {
WatcherDoneCh chan<- *ClientCallbackHandlerRequest
GlobalVaultAuthOptions *common.GlobalVaultAuthOptions
CredentialProviderFactory credentials.CredentialProviderFactory
UserAgent string
}

func defaultClientOptions() *ClientOptions {
Expand Down Expand Up @@ -801,6 +802,17 @@ func (c *defaultClient) init(ctx context.Context, client ctrlclient.Client,
if err != nil {
return err
}

// set the User-Agent header
if cfg.Headers == nil {
cfg.Headers = make(http.Header)
}
if opts.UserAgent != "" {
cfg.Headers.Add(consts.HeaderUserAgent, opts.UserAgent)
} else {
cfg.Headers.Add(consts.HeaderUserAgent, common.DefaultVSOUserAgent)
}

vc, err := MakeVaultClient(ctx, cfg, client)
if err != nil {
return err
Expand Down Expand Up @@ -921,13 +933,20 @@ func NewClientConfigFromConnObj(connObj *secretsv1beta1.VaultConnection, vaultNS
return nil, errors.New("invalid nil VaultConnection")
}

headers := make(http.Header)
if connObj.Spec.Headers != nil {
for k, v := range connObj.Spec.Headers {
headers.Add(k, v)
}
}

cfg := &ClientConfig{
Address: connObj.Spec.Address,
SkipTLSVerify: connObj.Spec.SkipTLSVerify,
TLSServerName: connObj.Spec.TLSServerName,
K8sNamespace: connObj.Namespace,
CACertSecretRef: connObj.Spec.CACertSecretRef,
Headers: connObj.Spec.Headers,
Headers: headers,
VaultNamespace: vaultNS,
}

Expand Down
119 changes: 117 additions & 2 deletions vault/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
Expand All @@ -18,12 +19,15 @@ import (
"golang.org/x/crypto/blake2b"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

secretsv1beta1 "github.com/hashicorp/vault-secrets-operator/api/v1beta1"
"github.com/hashicorp/vault-secrets-operator/common"
"github.com/hashicorp/vault-secrets-operator/consts"
"github.com/hashicorp/vault-secrets-operator/credentials"
"github.com/hashicorp/vault-secrets-operator/credentials/provider"
"github.com/hashicorp/vault-secrets-operator/credentials/vault"
vaultcredsconsts "github.com/hashicorp/vault-secrets-operator/credentials/vault/consts"
Expand Down Expand Up @@ -1188,7 +1192,7 @@ func TestNewClientConfigFromConnObj(t *testing.T) {
connObj: connObjBase,
want: &ClientConfig{
Address: "https://vault.example.com",
Headers: map[string]string{"foo": "bar"},
Headers: http.Header{"Foo": []string{"bar"}},
TLSServerName: "baz.biff",
CACertSecretRef: "ca.crt",
SkipTLSVerify: true,
Expand All @@ -1201,7 +1205,7 @@ func TestNewClientConfigFromConnObj(t *testing.T) {
connObj: connObjEmptyTimeout,
want: &ClientConfig{
Address: "https://vault.example.com",
Headers: map[string]string{"foo": "bar"},
Headers: http.Header{"Foo": []string{"bar"}},
TLSServerName: "baz.biff",
CACertSecretRef: "ca.crt",
SkipTLSVerify: true,
Expand Down Expand Up @@ -1263,3 +1267,114 @@ func Test_defaultClient_Renewable(t *testing.T) {
})
}
}

func TestClient_UserAgentHeader(t *testing.T) {
t.Parallel()

tests := []struct {
name string
userAgent string
expectedUserAgent string
}{
{
name: "default-user-agent",
userAgent: "",
expectedUserAgent: common.DefaultVSOUserAgent,
},
{
name: "custom-user-agent",
userAgent: "custom-client/1.0.0",
expectedUserAgent: "custom-client/1.0.0",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

// Create a mock client that captures headers
var capturedHeaders http.Header
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedHeaders = r.Header.Clone()
// Return a successful auth response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
resp := map[string]interface{}{
"auth": map[string]interface{}{
"client_token": "test-token",
"accessor": "test-accessor",
"renewable": false,
"lease_duration": 3600,
},
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()

// Create test objects
vaultConnection := &secretsv1beta1.VaultConnection{
ObjectMeta: metav1.ObjectMeta{
Name: "test-connection",
Namespace: "test-ns",
},
Spec: secretsv1beta1.VaultConnectionSpec{
Address: server.URL,
},
}

vaultAuth := &secretsv1beta1.VaultAuth{
ObjectMeta: metav1.ObjectMeta{
Name: "test-auth",
Namespace: "test-ns",
},
Spec: secretsv1beta1.VaultAuthSpec{
Method: "kubernetes",
Mount: "kubernetes",
Kubernetes: &secretsv1beta1.VaultAuthConfigKubernetes{
Role: "test-role",
ServiceAccount: "test-sa",
},
},
}

serviceAccount := &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sa",
Namespace: "test-ns",
},
}

scheme := runtime.NewScheme()
require.NoError(t, secretsv1beta1.AddToScheme(scheme))
require.NoError(t, corev1.AddToScheme(scheme))

k8sClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(vaultConnection, vaultAuth, serviceAccount).
Build()

var opts *ClientOptions
if tt.userAgent != "" {
opts = &ClientOptions{
UserAgent: tt.userAgent,
CredentialProviderFactory: credentials.NewCredentialProviderFactory(),
}
}

// Create and initialize the client
client := &defaultClient{}
err := client.Init(context.Background(), k8sClient, vaultAuth, vaultConnection, "test-ns", opts)
require.NoError(t, err)

// Perform a login to trigger HTTP request with headers
err = client.Login(context.Background(), k8sClient)
require.NoError(t, err)

// Verify that the User-Agent header was set correctly
require.NotNil(t, capturedHeaders)
userAgentHeaders := capturedHeaders.Values(consts.HeaderUserAgent)
require.Len(t, userAgentHeaders, 1, "Expected exactly one User-Agent header")
assert.Equal(t, tt.expectedUserAgent, userAgentHeaders[0])
})
}
}
9 changes: 6 additions & 3 deletions vault/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"crypto/x509"
"fmt"
"net/http"
"time"

"github.com/hashicorp/vault/api"
Expand Down Expand Up @@ -38,7 +39,7 @@ type ClientConfig struct {
// VaultNamespace is the namespace in Vault to auth to
VaultNamespace string
// Headers are http headers to set on the Vault client
Headers map[string]string
Headers http.Header
// Timeout applied to all Vault requests. If not set, the default timeout from
// the Vault API client config is used.
Timeout *time.Duration
Expand Down Expand Up @@ -108,8 +109,10 @@ func MakeVaultClient(ctx context.Context, cfg *ClientConfig, client ctrlclient.C
if _, exists := cfg.Headers[vconsts.NamespaceHeaderName]; exists {
return nil, fmt.Errorf("setting header %q on VaultConnection is not permitted", vconsts.NamespaceHeaderName)
}
for k, v := range cfg.Headers {
c.AddHeader(k, v)
for k, values := range cfg.Headers {
for _, v := range values {
c.AddHeader(k, v)
}
}
if cfg.VaultNamespace != "" {
c.SetNamespace(cfg.VaultNamespace)
Expand Down
22 changes: 12 additions & 10 deletions vault/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ func TestMakeVaultClient(t *testing.T) {
},
"headers": {
vaultConfig: &ClientConfig{
Headers: map[string]string{
"X-Proxy-Setting": "yes",
"Y-Proxy-Setting": "no",
Headers: http.Header{
"X-Proxy-Setting": []string{"yes"},
"Y-Proxy-Setting": []string{"no"},
},
VaultNamespace: "vault-test-namespace",
},
Expand All @@ -100,10 +100,10 @@ func TestMakeVaultClient(t *testing.T) {
},
"headers can't override namespace": {
vaultConfig: &ClientConfig{
Headers: map[string]string{
"X-Proxy-Setting": "yes",
"Y-Proxy-Setting": "no",
vconsts.NamespaceHeaderName: "nope",
Headers: http.Header{
"X-Proxy-Setting": []string{"yes"},
"Y-Proxy-Setting": []string{"no"},
vconsts.NamespaceHeaderName: []string{"nope"},
},
VaultNamespace: "vault-test-namespace",
},
Expand Down Expand Up @@ -175,12 +175,14 @@ func TestMakeVaultClient(t *testing.T) {
}
}

func makeVaultHttpHeaders(t *testing.T, namespace string, headers map[string]string) http.Header {
func makeVaultHttpHeaders(t *testing.T, namespace string, headers http.Header) http.Header {
t.Helper()

h := make(http.Header)
for k, v := range headers {
h.Set(k, v)
for k, values := range headers {
for _, v := range values {
h.Add(k, v)
}
}
h.Set("X-Vault-Request", "true")
if namespace != "" {
Expand Down
Loading