Skip to content

Commit d1ed24f

Browse files
Merge pull request #435 from husira/develop
feat: add bearer token support for http sync.go
2 parents 1205168 + 36ffbd5 commit d1ed24f

3 files changed

Lines changed: 243 additions & 3 deletions

File tree

pkg/vendir/config/data.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package config
66
const (
77
SecretK8sCorev1BasicAuthUsernameKey = "username"
88
SecretK8sCorev1BasicAuthPasswordKey = "password"
9+
SecretK8sCorev1HTTPBearerTokenKey = "token"
910

1011
SecretK8sCoreV1SSHAuthPrivateKey = "ssh-privatekey"
1112
SecretSSHAuthKnownHosts = "ssh-knownhosts" // not part of k8s

pkg/vendir/fetch/http/sync.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,56 @@ func (t *Sync) addAuth(req *http.Request) error {
148148
switch name {
149149
case ctlconf.SecretK8sCorev1BasicAuthUsernameKey:
150150
case ctlconf.SecretK8sCorev1BasicAuthPasswordKey:
151+
case ctlconf.SecretK8sCorev1HTTPBearerTokenKey:
151152
default:
152153
return fmt.Errorf("Unknown secret field '%s' in secret '%s'", name, secret.Metadata.Name)
153154
}
154155
}
155156

156-
if _, found := secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]; found {
157-
req.SetBasicAuth(string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]),
158-
string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey]))
157+
_, hasUser := secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]
158+
_, hasPass := secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey]
159+
token, hasToken := secret.Data[ctlconf.SecretK8sCorev1HTTPBearerTokenKey]
160+
161+
// Validate that token is not empty if provided.
162+
if hasToken && len(token) == 0 {
163+
return fmt.Errorf(
164+
"Secret '%s' contains empty '%s'",
165+
secret.Metadata.Name,
166+
ctlconf.SecretK8sCorev1HTTPBearerTokenKey,
167+
)
168+
}
169+
170+
// Basic auth requires a username if password is provided, but password is optional.
171+
if hasPass && !hasUser {
172+
return fmt.Errorf(
173+
"Secret '%s' contains '%s' but is missing '%s'",
174+
secret.Metadata.Name,
175+
ctlconf.SecretK8sCorev1BasicAuthPasswordKey,
176+
ctlconf.SecretK8sCorev1BasicAuthUsernameKey,
177+
)
178+
}
179+
180+
// Do not allow mixing basic auth and bearer token in the same secret.
181+
if hasToken && hasUser {
182+
return fmt.Errorf(
183+
"Secret '%s' must not contain both basic auth (username/password) and token",
184+
secret.Metadata.Name,
185+
)
186+
}
187+
188+
// Bearer token auth
189+
if hasToken {
190+
req.Header.Set("Authorization", "Bearer "+string(token))
191+
return nil
192+
}
193+
194+
// Basic auth — password is optional, defaults to empty string
195+
if hasUser {
196+
password := string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey])
197+
req.SetBasicAuth(
198+
string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]),
199+
password,
200+
)
159201
}
160202

161203
return nil

pkg/vendir/fetch/http/sync_test.go

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Copyright 2024 The Carvel Authors.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package http_test
5+
6+
import (
7+
"fmt"
8+
"net/http"
9+
"net/http/httptest"
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
ctlconf "carvel.dev/vendir/pkg/vendir/config"
15+
vendirhttp "carvel.dev/vendir/pkg/vendir/fetch/http"
16+
"github.com/stretchr/testify/require"
17+
)
18+
19+
type fakeRefFetcher struct {
20+
secrets map[string]ctlconf.Secret
21+
configMaps map[string]ctlconf.ConfigMap
22+
}
23+
24+
func (f fakeRefFetcher) GetSecret(name string) (ctlconf.Secret, error) {
25+
s, ok := f.secrets[name]
26+
if !ok {
27+
return ctlconf.Secret{}, fmt.Errorf("secret %q not found", name)
28+
}
29+
return s, nil
30+
}
31+
32+
func (f fakeRefFetcher) GetConfigMap(name string) (ctlconf.ConfigMap, error) {
33+
if f.configMaps == nil {
34+
return ctlconf.ConfigMap{}, fmt.Errorf("configmap %q not found", name)
35+
}
36+
37+
cm, ok := f.configMaps[name]
38+
if !ok {
39+
return ctlconf.ConfigMap{}, fmt.Errorf("configmap %q not found", name)
40+
}
41+
return cm, nil
42+
}
43+
44+
type fakeTempArea struct {
45+
baseDir string
46+
}
47+
48+
func (f fakeTempArea) NewTempDir(prefix string) (string, error) {
49+
return os.MkdirTemp(f.baseDir, prefix)
50+
}
51+
52+
func (f fakeTempArea) NewTempFile(prefix string) (*os.File, error) {
53+
return os.CreateTemp(f.baseDir, prefix)
54+
}
55+
56+
func secretRef(name string) *ctlconf.DirectoryContentsLocalRef {
57+
return &ctlconf.DirectoryContentsLocalRef{Name: name}
58+
}
59+
60+
type syncTest struct {
61+
name string
62+
secret ctlconf.Secret
63+
expectedBody string
64+
expectedError string
65+
validateReq func(t *testing.T, r *http.Request)
66+
}
67+
68+
func TestSync_HTTPAuth(t *testing.T) {
69+
allTests := []syncTest{
70+
{
71+
name: "when basic auth username and password are provided, it succeeds",
72+
secret: ctlconf.Secret{
73+
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
74+
Data: map[string][]byte{
75+
ctlconf.SecretK8sCorev1BasicAuthUsernameKey: []byte("admin"),
76+
ctlconf.SecretK8sCorev1BasicAuthPasswordKey: []byte("password"),
77+
},
78+
},
79+
expectedBody: "ok",
80+
validateReq: func(t *testing.T, r *http.Request) {
81+
user, pass, ok := r.BasicAuth()
82+
require.True(t, ok)
83+
require.Equal(t, "admin", user)
84+
require.Equal(t, "password", pass)
85+
},
86+
},
87+
{
88+
name: "when bearer token is provided, it succeeds",
89+
secret: ctlconf.Secret{
90+
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
91+
Data: map[string][]byte{
92+
ctlconf.SecretK8sCorev1HTTPBearerTokenKey: []byte("abc123"),
93+
},
94+
},
95+
expectedBody: "ok",
96+
validateReq: func(t *testing.T, r *http.Request) {
97+
require.Equal(t, "Bearer abc123", r.Header.Get("Authorization"))
98+
},
99+
},
100+
{
101+
name: "when username is provided without password, it uses empty password and succeeds",
102+
secret: ctlconf.Secret{
103+
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
104+
Data: map[string][]byte{
105+
ctlconf.SecretK8sCorev1BasicAuthUsernameKey: []byte("admin"),
106+
},
107+
},
108+
expectedBody: "ok",
109+
validateReq: func(t *testing.T, r *http.Request) {
110+
user, pass, ok := r.BasicAuth()
111+
require.True(t, ok)
112+
require.Equal(t, "admin", user)
113+
require.Equal(t, "", pass)
114+
},
115+
},
116+
{
117+
name: "when basic auth and bearer token are mixed, it fails",
118+
secret: ctlconf.Secret{
119+
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
120+
Data: map[string][]byte{
121+
ctlconf.SecretK8sCorev1BasicAuthUsernameKey: []byte("admin"),
122+
ctlconf.SecretK8sCorev1BasicAuthPasswordKey: []byte("password"),
123+
ctlconf.SecretK8sCorev1HTTPBearerTokenKey: []byte("abc123"),
124+
},
125+
},
126+
expectedError: "must not contain both basic auth",
127+
},
128+
{
129+
name: "when bearer token is empty, it fails",
130+
secret: ctlconf.Secret{
131+
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
132+
Data: map[string][]byte{
133+
ctlconf.SecretK8sCorev1HTTPBearerTokenKey: []byte(""),
134+
},
135+
},
136+
expectedError: "contains empty 'token'",
137+
},
138+
{
139+
name: "when password is provided without username, it fails",
140+
secret: ctlconf.Secret{
141+
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
142+
Data: map[string][]byte{
143+
ctlconf.SecretK8sCorev1BasicAuthPasswordKey: []byte("password"),
144+
},
145+
},
146+
expectedError: "is missing 'username'",
147+
},
148+
}
149+
150+
for _, test := range allTests {
151+
t.Run(test.name, func(t *testing.T) {
152+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
153+
if test.expectedError != "" {
154+
t.Fatalf("server should not be reached when auth setup fails")
155+
}
156+
157+
test.validateReq(t, r)
158+
159+
w.WriteHeader(http.StatusOK)
160+
_, err := w.Write([]byte(test.expectedBody))
161+
require.NoError(t, err)
162+
}))
163+
defer srv.Close()
164+
165+
ref := fakeRefFetcher{
166+
secrets: map[string]ctlconf.Secret{
167+
"http-auth": test.secret,
168+
},
169+
}
170+
171+
subject := vendirhttp.NewSync(ctlconf.DirectoryContentsHTTP{
172+
URL: srv.URL,
173+
SecretRef: secretRef("http-auth"),
174+
DisableUnpack: true,
175+
}, ref)
176+
177+
tempRoot, err := os.MkdirTemp("", "vendir-http-test")
178+
require.NoError(t, err)
179+
defer os.RemoveAll(tempRoot)
180+
181+
dstPath := filepath.Join(tempRoot, "dst")
182+
_, err = subject.Sync(dstPath, fakeTempArea{baseDir: tempRoot})
183+
184+
if test.expectedError != "" {
185+
require.Error(t, err)
186+
require.Contains(t, err.Error(), test.expectedError)
187+
return
188+
}
189+
190+
require.NoError(t, err)
191+
192+
bs, err := os.ReadFile(filepath.Join(dstPath, filepath.Base(srv.URL)))
193+
require.NoError(t, err)
194+
require.Equal(t, test.expectedBody, string(bs))
195+
})
196+
}
197+
}

0 commit comments

Comments
 (0)