Add support for retreiving registry credentials from netrc files

Signed-off-by: Clark Tomlinson <fallen013@gmail.com>
pull/13553/head
Clark Tomlinson 9 months ago
parent b97ed6545a
commit c55b81e10c
No known key found for this signature in database
GPG Key ID: 177547AA11DDDC04

@ -0,0 +1,157 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package netrc
import (
"net/url"
"os"
"path/filepath"
"strings"
)
// DefaultPath returns the default path to the .netrc file
func DefaultPath() string {
if os.Getenv("NETRC") != "" {
return os.Getenv("NETRC")
}
return filepath.Join(os.Getenv("HOME"), ".netrc")
}
// Credentials represents a machine entry in .netrc file
type Credentials struct {
Machine string
Login string
Password string
}
// GetCredentials returns the credentials for the given URL from .netrc file
func GetCredentials(urlStr string) (*Credentials, error) {
netrcPath := DefaultPath()
if _, err := os.Stat(netrcPath); os.IsNotExist(err) {
return nil, nil
}
data, err := os.ReadFile(netrcPath)
if err != nil {
return nil, err
}
u, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
host := u.Host
if strings.Contains(host, ":") {
host = strings.Split(host, ":")[0]
}
parser := newParser(string(data))
machines, err := parser.parse()
if err != nil {
return nil, err
}
for _, m := range machines {
if m.Machine == host {
return &Credentials{
Machine: m.Machine,
Login: m.Login,
Password: m.Password,
}, nil
}
}
return nil, nil
}
type machine struct {
Machine string
Login string
Password string
}
type parser struct {
input string
pos int
}
func newParser(input string) *parser {
return &parser{input: input}
}
func (p *parser) parse() ([]machine, error) {
var machines []machine
var current machine
for p.pos < len(p.input) {
token := p.nextToken()
if token == "" {
break
}
switch token {
case "machine":
if current.Machine != "" {
machines = append(machines, current)
current = machine{}
}
current.Machine = p.nextToken()
case "login":
current.Login = p.nextToken()
case "password":
current.Password = p.nextToken()
}
}
if current.Machine != "" {
machines = append(machines, current)
}
return machines, nil
}
func (p *parser) nextToken() string {
// Skip whitespace
for p.pos < len(p.input) && (p.input[p.pos] == ' ' || p.input[p.pos] == '\t' || p.input[p.pos] == '\n' || p.input[p.pos] == '\r') {
p.pos++
}
if p.pos >= len(p.input) {
return ""
}
start := p.pos
if p.input[p.pos] == '"' {
p.pos++
start = p.pos
for p.pos < len(p.input) && p.input[p.pos] != '"' {
p.pos++
}
if p.pos < len(p.input) {
token := p.input[start:p.pos]
p.pos++
return token
}
} else {
for p.pos < len(p.input) && p.input[p.pos] != ' ' && p.input[p.pos] != '\t' && p.input[p.pos] != '\n' && p.input[p.pos] != '\r' {
p.pos++
}
}
return p.input[start:p.pos]
}

@ -0,0 +1,143 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package netrc
import (
"os"
"path/filepath"
"testing"
)
func TestGetCredentials(t *testing.T) {
// Create a temporary .netrc file
content := `machine example.com
login testuser
password testpass
machine other.com
login otheruser
password otherpass`
tmpDir := t.TempDir()
netrcPath := filepath.Join(tmpDir, ".netrc")
if err := os.WriteFile(netrcPath, []byte(content), 0600); err != nil {
t.Fatal(err)
}
// Set NETRC env var to point to our test file
oldNetrc := os.Getenv("NETRC")
defer os.Setenv("NETRC", oldNetrc)
os.Setenv("NETRC", netrcPath)
tests := []struct {
name string
url string
wantUser string
wantPass string
wantErr bool
}{
{
name: "basic URL",
url: "https://example.com/repo",
wantUser: "testuser",
wantPass: "testpass",
},
{
name: "URL with port",
url: "https://example.com:443/repo",
wantUser: "testuser",
wantPass: "testpass",
},
{
name: "other domain",
url: "https://other.com/repo",
wantUser: "otheruser",
wantPass: "otherpass",
},
{
name: "no match",
url: "https://nomatch.com/repo",
wantUser: "",
wantPass: "",
},
{
name: "invalid URL",
url: "://invalid",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetCredentials(tt.url)
if (err != nil) != tt.wantErr {
t.Errorf("GetCredentials() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr {
return
}
if tt.wantUser == "" && tt.wantPass == "" {
if got != nil {
t.Errorf("GetCredentials() = %v, want nil", got)
}
return
}
if got == nil {
t.Fatal("GetCredentials() = nil, want credentials")
}
if got.Login != tt.wantUser {
t.Errorf("GetCredentials() username = %v, want %v", got.Login, tt.wantUser)
}
if got.Password != tt.wantPass {
t.Errorf("GetCredentials() password = %v, want %v", got.Password, tt.wantPass)
}
})
}
}
func TestParseNetrc(t *testing.T) {
content := `# comment line
machine example.com
login user1
password pass1
machine other.com login user2 password pass2
machine "quoted.com"
login "user 3"
password "pass 3"
`
p := newParser(content)
machines, err := p.parse()
if err != nil {
t.Fatal(err)
}
if len(machines) != 3 {
t.Errorf("Expected 3 machines, got %d", len(machines))
}
expected := []machine{
{Machine: "example.com", Login: "user1", Password: "pass1"},
{Machine: "other.com", Login: "user2", Password: "pass2"},
{Machine: "quoted.com", Login: "user 3", Password: "pass 3"},
}
for i, e := range expected {
if machines[i] != e {
t.Errorf("machine[%d] = %v, want %v", i, machines[i], e)
}
}
}

@ -41,6 +41,7 @@ import (
"helm.sh/helm/v3/internal/version" "helm.sh/helm/v3/internal/version"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/helmpath" "helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/netrc"
) )
// See https://github.com/helm/helm/issues/10166 // See https://github.com/helm/helm/issues/10166
@ -117,9 +118,20 @@ func NewClient(options ...ClientOption) (*Client, error) {
} }
// if username and password are set, use them for authentication // if username and password are set, use them for authentication
// by adding the basic auth Authorization header to the resolver username := client.username
if client.username != "" && client.password != "" { password := client.password
concat := client.username + ":" + client.password
// If credentials are not explicitly set, try to get them from .netrc
if username == "" && password == "" {
if creds, err := netrc.GetCredentials(ref.Registry); err == nil && creds != nil {
username = creds.Login
password = creds.Password
}
}
// Add credentials to resolver if available
if username != "" && password != "" {
concat := username + ":" + password
encodedAuth := base64.StdEncoding.EncodeToString([]byte(concat)) encodedAuth := base64.StdEncoding.EncodeToString([]byte(concat))
opts = append(opts, auth.WithResolverHeaders( opts = append(opts, auth.WithResolverHeaders(
http.Header{ http.Header{
@ -148,10 +160,21 @@ func NewClient(options ...ClientOption) (*Client, error) {
}, },
Cache: cache, Cache: cache,
Credential: func(_ context.Context, reg string) (registryauth.Credential, error) { Credential: func(_ context.Context, reg string) (registryauth.Credential, error) {
if client.username != "" && client.password != "" { username := client.username
password := client.password
// If credentials are not explicitly set, try to get them from .netrc
if username == "" && password == "" {
if creds, err := netrc.GetCredentials(reg); err == nil && creds != nil {
username = creds.Login
password = creds.Password
}
}
if username != "" && password != "" {
return registryauth.Credential{ return registryauth.Credential{
Username: client.username, Username: username,
Password: client.password, Password: password,
}, nil }, nil
} }
@ -176,10 +199,8 @@ func NewClient(options ...ClientOption) (*Client, error) {
Username: username, Username: username,
Password: password, Password: password,
}, nil }, nil
}, },
} }
} }
return client, nil return client, nil
} }

@ -0,0 +1,79 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"os"
"path/filepath"
"testing"
"helm.sh/helm/v3/pkg/netrc"
)
func TestClientNetrcAuth(t *testing.T) {
// Create a temporary .netrc file
content := `machine example.com
login testuser
password testpass`
tmpDir := t.TempDir()
netrcPath := filepath.Join(tmpDir, ".netrc")
if err := os.WriteFile(netrcPath, []byte(content), 0600); err != nil {
t.Fatal(err)
}
// Set NETRC env var to point to our test file
oldNetrc := os.Getenv("NETRC")
defer os.Setenv("NETRC", oldNetrc)
os.Setenv("NETRC", netrcPath)
// Create a new client
client, err := NewClient()
if err != nil {
t.Fatal(err)
}
// Test that credentials from .netrc are used
creds, err := netrc.GetCredentials("https://example.com")
if err != nil {
t.Fatal(err)
}
if creds == nil {
t.Fatal("Expected credentials from .netrc, got nil")
}
if creds.Login != "testuser" {
t.Errorf("Expected username 'testuser', got '%s'", creds.Login)
}
if creds.Password != "testpass" {
t.Errorf("Expected password 'testpass', got '%s'", creds.Password)
}
// Test that explicit credentials override .netrc
client, err = NewClient(
ClientOptBasicAuth("explicituser", "explicitpass"),
)
if err != nil {
t.Fatal(err)
}
if client.username != "explicituser" {
t.Errorf("Expected username 'explicituser', got '%s'", client.username)
}
if client.password != "explicitpass" {
t.Errorf("Expected password 'explicitpass', got '%s'", client.password)
}
}
Loading…
Cancel
Save