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
1 change: 1 addition & 0 deletions pkg/vendir/config/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package config
const (
SecretK8sCorev1BasicAuthUsernameKey = "username"
SecretK8sCorev1BasicAuthPasswordKey = "password"
SecretK8sCorev1HTTPBearerTokenKey = "token"

SecretK8sCoreV1SSHAuthPrivateKey = "ssh-privatekey"
SecretSSHAuthKnownHosts = "ssh-knownhosts" // not part of k8s
Expand Down
48 changes: 45 additions & 3 deletions pkg/vendir/fetch/http/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,14 +148,56 @@
switch name {
case ctlconf.SecretK8sCorev1BasicAuthUsernameKey:
case ctlconf.SecretK8sCorev1BasicAuthPasswordKey:
case ctlconf.SecretK8sCorev1HTTPBearerTokenKey:
default:
return fmt.Errorf("Unknown secret field '%s' in secret '%s'", name, secret.Metadata.Name)
}
}

if _, found := secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]; found {
req.SetBasicAuth(string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]),
string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey]))
_, hasUser := secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]
_, hasPass := secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey]
token, hasToken := secret.Data[ctlconf.SecretK8sCorev1HTTPBearerTokenKey]

// Validate that token is not empty if provided.
if hasToken && len(token) == 0 {

Check failure on line 162 in pkg/vendir/fetch/http/sync.go

View workflow job for this annotation

GitHub Actions / lint

add-constant: avoid magic numbers like '0', create a named constant for it (revive)
return fmt.Errorf(

Check failure on line 163 in pkg/vendir/fetch/http/sync.go

View workflow job for this annotation

GitHub Actions / lint

ST1005: error strings should not be capitalized (staticcheck)
"Secret '%s' contains empty '%s'",
secret.Metadata.Name,
ctlconf.SecretK8sCorev1HTTPBearerTokenKey,
)
}

// Basic auth requires a username if password is provided, but password is optional.
if hasPass && !hasUser {
return fmt.Errorf(

Check failure on line 172 in pkg/vendir/fetch/http/sync.go

View workflow job for this annotation

GitHub Actions / lint

ST1005: error strings should not be capitalized (staticcheck)
"Secret '%s' contains '%s' but is missing '%s'",
secret.Metadata.Name,
ctlconf.SecretK8sCorev1BasicAuthPasswordKey,
ctlconf.SecretK8sCorev1BasicAuthUsernameKey,
)
}

// Do not allow mixing basic auth and bearer token in the same secret.
if hasToken && hasUser {
return fmt.Errorf(

Check failure on line 182 in pkg/vendir/fetch/http/sync.go

View workflow job for this annotation

GitHub Actions / lint

ST1005: error strings should not be capitalized (staticcheck)
"Secret '%s' must not contain both basic auth (username/password) and token",
secret.Metadata.Name,
)
}
Comment thread
husira marked this conversation as resolved.

// Bearer token auth
if hasToken {
req.Header.Set("Authorization", "Bearer "+string(token))
return nil
}
Comment thread
husira marked this conversation as resolved.
Comment thread
husira marked this conversation as resolved.

// Basic auth — password is optional, defaults to empty string
if hasUser {
password := string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthPasswordKey])
req.SetBasicAuth(
string(secret.Data[ctlconf.SecretK8sCorev1BasicAuthUsernameKey]),
password,
)
}

return nil
Expand Down
197 changes: 197 additions & 0 deletions pkg/vendir/fetch/http/sync_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// Copyright 2024 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package http_test

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

ctlconf "carvel.dev/vendir/pkg/vendir/config"
vendirhttp "carvel.dev/vendir/pkg/vendir/fetch/http"
"github.com/stretchr/testify/require"
)

type fakeRefFetcher struct {
secrets map[string]ctlconf.Secret
configMaps map[string]ctlconf.ConfigMap
}

func (f fakeRefFetcher) GetSecret(name string) (ctlconf.Secret, error) {
s, ok := f.secrets[name]
if !ok {
return ctlconf.Secret{}, fmt.Errorf("secret %q not found", name)
}
return s, nil
}

func (f fakeRefFetcher) GetConfigMap(name string) (ctlconf.ConfigMap, error) {
if f.configMaps == nil {
return ctlconf.ConfigMap{}, fmt.Errorf("configmap %q not found", name)
}

cm, ok := f.configMaps[name]
if !ok {
return ctlconf.ConfigMap{}, fmt.Errorf("configmap %q not found", name)
}
return cm, nil
}

type fakeTempArea struct {
baseDir string
}

func (f fakeTempArea) NewTempDir(prefix string) (string, error) {
return os.MkdirTemp(f.baseDir, prefix)
}

func (f fakeTempArea) NewTempFile(prefix string) (*os.File, error) {
return os.CreateTemp(f.baseDir, prefix)
}

func secretRef(name string) *ctlconf.DirectoryContentsLocalRef {
return &ctlconf.DirectoryContentsLocalRef{Name: name}
}

type syncTest struct {
name string
secret ctlconf.Secret
expectedBody string
expectedError string
validateReq func(t *testing.T, r *http.Request)
}

func TestSync_HTTPAuth(t *testing.T) {

Check failure on line 68 in pkg/vendir/fetch/http/sync_test.go

View workflow job for this annotation

GitHub Actions / lint

function-length: maximum number of lines per function exceeded; max 75 but got 128 (revive)
allTests := []syncTest{
{
name: "when basic auth username and password are provided, it succeeds",
secret: ctlconf.Secret{
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
Data: map[string][]byte{
ctlconf.SecretK8sCorev1BasicAuthUsernameKey: []byte("admin"),
ctlconf.SecretK8sCorev1BasicAuthPasswordKey: []byte("password"),
},
},
expectedBody: "ok",
validateReq: func(t *testing.T, r *http.Request) {
user, pass, ok := r.BasicAuth()
require.True(t, ok)
require.Equal(t, "admin", user)
require.Equal(t, "password", pass)
},
},
{
name: "when bearer token is provided, it succeeds",
secret: ctlconf.Secret{
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
Data: map[string][]byte{
ctlconf.SecretK8sCorev1HTTPBearerTokenKey: []byte("abc123"),
},
},
expectedBody: "ok",
validateReq: func(t *testing.T, r *http.Request) {
require.Equal(t, "Bearer abc123", r.Header.Get("Authorization"))
},
},
{
name: "when username is provided without password, it uses empty password and succeeds",
secret: ctlconf.Secret{
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},

Check failure on line 103 in pkg/vendir/fetch/http/sync_test.go

View workflow job for this annotation

GitHub Actions / lint

add-constant: string literal "http-auth" appears, at least, 3 times, create a named constant for it (revive)
Data: map[string][]byte{
ctlconf.SecretK8sCorev1BasicAuthUsernameKey: []byte("admin"),

Check failure on line 105 in pkg/vendir/fetch/http/sync_test.go

View workflow job for this annotation

GitHub Actions / lint

add-constant: string literal "admin" appears, at least, 3 times, create a named constant for it (revive)
},
},
expectedBody: "ok",

Check failure on line 108 in pkg/vendir/fetch/http/sync_test.go

View workflow job for this annotation

GitHub Actions / lint

add-constant: string literal "ok" appears, at least, 3 times, create a named constant for it (revive)
validateReq: func(t *testing.T, r *http.Request) {
user, pass, ok := r.BasicAuth()
require.True(t, ok)
require.Equal(t, "admin", user)
require.Equal(t, "", pass)
},
Comment thread
joaopapereira marked this conversation as resolved.
},
{
name: "when basic auth and bearer token are mixed, it fails",
secret: ctlconf.Secret{
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
Data: map[string][]byte{
ctlconf.SecretK8sCorev1BasicAuthUsernameKey: []byte("admin"),
ctlconf.SecretK8sCorev1BasicAuthPasswordKey: []byte("password"),

Check failure on line 122 in pkg/vendir/fetch/http/sync_test.go

View workflow job for this annotation

GitHub Actions / lint

add-constant: string literal "password" appears, at least, 3 times, create a named constant for it (revive)
ctlconf.SecretK8sCorev1HTTPBearerTokenKey: []byte("abc123"),
},
},
expectedError: "must not contain both basic auth",
},
{
name: "when bearer token is empty, it fails",
secret: ctlconf.Secret{
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
Data: map[string][]byte{
ctlconf.SecretK8sCorev1HTTPBearerTokenKey: []byte(""),
},
},
expectedError: "contains empty 'token'",
},
{
name: "when password is provided without username, it fails",
secret: ctlconf.Secret{
Metadata: ctlconf.GenericMetadata{Name: "http-auth"},
Data: map[string][]byte{
ctlconf.SecretK8sCorev1BasicAuthPasswordKey: []byte("password"),
},
},
expectedError: "is missing 'username'",
},
}

for _, test := range allTests {
t.Run(test.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if test.expectedError != "" {
t.Fatalf("server should not be reached when auth setup fails")
}

test.validateReq(t, r)

w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(test.expectedBody))
require.NoError(t, err)
}))
Comment thread
husira marked this conversation as resolved.
defer srv.Close()

ref := fakeRefFetcher{
secrets: map[string]ctlconf.Secret{
"http-auth": test.secret,
},
}

subject := vendirhttp.NewSync(ctlconf.DirectoryContentsHTTP{
URL: srv.URL,
SecretRef: secretRef("http-auth"),
DisableUnpack: true,
}, ref)

tempRoot, err := os.MkdirTemp("", "vendir-http-test")
require.NoError(t, err)
defer os.RemoveAll(tempRoot)

dstPath := filepath.Join(tempRoot, "dst")
_, err = subject.Sync(dstPath, fakeTempArea{baseDir: tempRoot})

if test.expectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), test.expectedError)
return
}

require.NoError(t, err)

bs, err := os.ReadFile(filepath.Join(dstPath, filepath.Base(srv.URL)))
Comment thread
husira marked this conversation as resolved.
require.NoError(t, err)
require.Equal(t, test.expectedBody, string(bs))
})
}
}
Loading