Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for file based configuration of basic auth username in http client config #511

Merged
merged 7 commits into from Sep 4, 2023
32 changes: 25 additions & 7 deletions config/http_config.go
Expand Up @@ -129,6 +129,7 @@ func (tv *TLSVersion) String() string {
// BasicAuth contains basic HTTP authentication credentials.
type BasicAuth struct {
Username string `yaml:"username" json:"username"`
UsernameFile string `yaml:"username_file,omitempty" json:"username_file,omitempty"`
Password Secret `yaml:"password,omitempty" json:"password,omitempty"`
PasswordFile string `yaml:"password_file,omitempty" json:"password_file,omitempty"`
}
Expand All @@ -139,6 +140,7 @@ func (a *BasicAuth) SetDirectory(dir string) {
return
}
a.PasswordFile = JoinDir(dir, a.PasswordFile)
a.UsernameFile = JoinDir(dir, a.UsernameFile)
}

// Authorization contains HTTP authorization credentials.
Expand Down Expand Up @@ -334,6 +336,9 @@ func (c *HTTPClientConfig) Validate() error {
if (c.BasicAuth != nil || c.OAuth2 != nil) && (len(c.BearerToken) > 0 || len(c.BearerTokenFile) > 0) {
return fmt.Errorf("at most one of basic_auth, oauth2, bearer_token & bearer_token_file must be configured")
}
if c.BasicAuth != nil && (string(c.BasicAuth.Username) != "" && c.BasicAuth.UsernameFile != "") {
return fmt.Errorf("at most one of basic_auth username & username_file must be configured")
}
if c.BasicAuth != nil && (string(c.BasicAuth.Password) != "" && c.BasicAuth.PasswordFile != "") {
return fmt.Errorf("at most one of basic_auth password & password_file must be configured")
}
Expand Down Expand Up @@ -555,7 +560,7 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
}

if cfg.BasicAuth != nil {
rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile, rt)
rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.UsernameFile, cfg.BasicAuth.PasswordFile, rt)
}

if cfg.OAuth2 != nil {
Expand Down Expand Up @@ -645,30 +650,43 @@ func (rt *authorizationCredentialsFileRoundTripper) CloseIdleConnections() {
type basicAuthRoundTripper struct {
username string
password Secret
usernameFile string
passwordFile string
rt http.RoundTripper
}

// NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a request unless it has
// already been set.
func NewBasicAuthRoundTripper(username string, password Secret, passwordFile string, rt http.RoundTripper) http.RoundTripper {
return &basicAuthRoundTripper{username, password, passwordFile, rt}
func NewBasicAuthRoundTripper(username string, password Secret, usernameFile, passwordFile string, rt http.RoundTripper) http.RoundTripper {
return &basicAuthRoundTripper{username, password, usernameFile, passwordFile, rt}
}

func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
var username string
var password string
if len(req.Header.Get("Authorization")) != 0 {
return rt.rt.RoundTrip(req)
}
req = cloneRequest(req)
if rt.usernameFile != "" {
usernameBytes, err := os.ReadFile(rt.usernameFile)
if err != nil {
return nil, fmt.Errorf("unable to read basic auth username file %s: %s", rt.usernameFile, err)
}
username = strings.TrimSpace(string(usernameBytes))
} else {
username = rt.username
}
if rt.passwordFile != "" {
bs, err := os.ReadFile(rt.passwordFile)
passwordBytes, err := os.ReadFile(rt.passwordFile)
if err != nil {
return nil, fmt.Errorf("unable to read basic auth password file %s: %s", rt.passwordFile, err)
}
req.SetBasicAuth(rt.username, strings.TrimSpace(string(bs)))
password = strings.TrimSpace(string(passwordBytes))
} else {
req.SetBasicAuth(rt.username, strings.TrimSpace(string(rt.password)))
password = string(rt.password)
}
req = cloneRequest(req)
req.SetBasicAuth(username, password)
return rt.rt.RoundTrip(req)
}

Expand Down
30 changes: 30 additions & 0 deletions config/http_config_test.go
Expand Up @@ -83,6 +83,10 @@ var invalidHTTPClientConfigs = []struct {
httpClientConfigFile: "testdata/http.conf.basic-auth.too-much.bad.yaml",
errMsg: "at most one of basic_auth password & password_file must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.basic-auth.bad-username.yaml",
errMsg: "at most one of basic_auth username & username_file must be configured",
},
{
httpClientConfigFile: "testdata/http.conf.mix-bearer-and-creds.bad.yaml",
errMsg: "authorization is not compatible with bearer_token & bearer_token_file",
Expand Down Expand Up @@ -896,6 +900,32 @@ func TestBasicAuthPasswordFile(t *testing.T) {
}
}

func TestBasicUsernameFile(t *testing.T) {
cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.basic-auth.username-file.good.yaml")
if err != nil {
t.Fatalf("Error loading HTTP client config: %v", err)
}
client, err := NewClientFromConfig(*cfg, "test")
if err != nil {
t.Fatalf("Error creating HTTP Client: %v", err)
}

rt, ok := client.Transport.(*basicAuthRoundTripper)
if !ok {
t.Fatalf("Error casting to basic auth transport, %v", client.Transport)
}

if rt.username != "" {
t.Errorf("Bad HTTP client username: %s", rt.username)
}
if string(rt.usernameFile) != "testdata/basic-auth-username" {
t.Errorf("Bad HTTP client usernameFile: %s", rt.usernameFile)
}
if string(rt.passwordFile) != "testdata/basic-auth-password" {
t.Errorf("Bad HTTP client passwordFile: %s", rt.passwordFile)
}
}

func getCertificateBlobs(t *testing.T) map[string][]byte {
files := []string{
TLSCAChainPath,
Expand Down
1 change: 1 addition & 0 deletions config/testdata/basic-auth-username
@@ -0,0 +1 @@
testuser
4 changes: 4 additions & 0 deletions config/testdata/http.conf.basic-auth.bad-username.yaml
@@ -0,0 +1,4 @@
basic_auth:
username: user
username_file: testdata/basic-auth-username
password: foo
3 changes: 3 additions & 0 deletions config/testdata/http.conf.basic-auth.username-file.good.yaml
@@ -0,0 +1,3 @@
basic_auth:
username_file: testdata/basic-auth-username
password_file: testdata/basic-auth-password