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

Add netrc-based AuthTransport #11

Merged
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ require (
github.com/google/go-cmp v0.5.8
github.com/google/go-querystring v1.1.0
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/jdx/go-netrc v1.0.0
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
github.com/trivago/tgo v1.0.7
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f // indirect
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
Expand Down
22 changes: 21 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
Expand All @@ -15,12 +16,25 @@ github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxC
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ=
github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -29,3 +43,9 @@ golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
141 changes: 141 additions & 0 deletions jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"time"

"github.com/jdx/go-netrc"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/google/go-querystring/query"
"github.com/hashicorp/go-retryablehttp"
Expand Down Expand Up @@ -403,6 +406,144 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
return defaultTransport
}

// netrcCredentials contains the login name and password for a particular machine, as
// defined by a netrc file.
type netrcCredentials struct {
Login string
Password string
}

// NetrcBasicAuthTransport is an http.RoundTripper that authenticates all requests
// using HTTP Basic Authentication with the machine login and password sourced from a
// netrc file.
//
// In the netrc file format, machine names are hostnames - they may not contain port
// numbers or URL paths. NetrcBasicAuthTransport may therefore behave incorrectly if the
// netrc file contains credentials for multiple Jira API instances hosted on the same
// host but on different ports or at different URL paths.
//
// netrc file format reference: https://www.gnu.org/software/inetutils/manual/html_node/The-_002enetrc-file.html
type NetrcBasicAuthTransport struct {
// Path is the path to the netrc file containing the Jira credentials. If nil, the
// .netrc file in the user's home directory is used.
Path *string

// Transport is the underlying HTTP transport to use when making requests.
// If nil, defaults to a Transport that automatically retries the request on failure.
Transport http.RoundTripper

netrcFile *netrc.Netrc
cache map[string]*netrcCredentials
}

// DefaultNetrcBasicAuthTransport creates a NetrcBasicAuthTransport from the credentials
// stored in ~/.netrc.
func DefaultNetrcBasicAuthTransport() *NetrcBasicAuthTransport {
return &NetrcBasicAuthTransport{
Path: nil,
cache: make(map[string]*netrcCredentials),
}
}

// NewNetrcBasicAuthTransport creates a NetrcBasicAuthTransport from the credentials stored in the
// netrc file at the given path.
func NewNetrcBasicAuthTransport(path string) *NetrcBasicAuthTransport {
return &NetrcBasicAuthTransport{
Path: &path,
cache: make(map[string]*netrcCredentials),
}
}

// RoundTrip implements the RoundTripper interface. We just add the credentials and return the
// RoundTripper for this transport type.
func (t *NetrcBasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract

creds, err := t.credentials(req.URL.Hostname())
if err != nil {
return nil, err
}

req2.SetBasicAuth(creds.Login, creds.Password)
return t.transport().RoundTrip(req2)
}

func (t *NetrcBasicAuthTransport) parseNetrcFile() error {
if t.Path == nil {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get path to user's home directory: %w", err)
}
p := filepath.Join(homeDir, ".netrc")
t.Path = &p
}

n, err := netrc.Parse(*t.Path)
if err != nil {
return fmt.Errorf("%s: parsing failure: %w", *t.Path, err)
}
t.netrcFile = n

return nil
}

func (t *NetrcBasicAuthTransport) credentials(host string) (*netrcCredentials, error) {
if t.netrcFile == nil {
err := t.parseNetrcFile()
if err != nil {
return nil, err
}
}

creds, exists := t.cache[host]
if !exists {
machine := t.netrcFile.Machine(host)
if machine == nil {
return nil, fmt.Errorf("%s: no credentials for machine '%s'", *t.Path, host)
}
if machine.Get("login") == "" {
return nil, fmt.Errorf("%s: no login for machine '%s'", *t.Path, host)
}
if machine.Get("password") == "" {
return nil, fmt.Errorf("%s: no password for machine '%s'", *t.Path, host)
}
t.cache[host] = &netrcCredentials{
Login: machine.Get("login"),
Password: machine.Get("password"),
}
creds = t.cache[host]
}

return creds, nil
}

// Username returns the HTTP Basic Authentication username that would be used to authenticate
// with the Jira API at the given hostname.
func (t *NetrcBasicAuthTransport) Username(host string) (string, error) {
creds, err := t.credentials(host)
if err != nil {
return "", err
}
return creds.Login, nil
}

// Client returns an *http.Client that makes requests that are authenticated using HTTP Basic
// Authentication with credentials from a netrc file. This is a nice little bit of sugar so
// we can just get the client instead of creating the client in the calling code.
//
// If it's necessary to send more information on client init, the calling code can always skip this
// and set the transport itself.
func (t *NetrcBasicAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

func (t *NetrcBasicAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return defaultTransport
}

// BearerAuthTransport is a http.RoundTripper that authenticates all requests
// using Jira's bearer (oauth 2.0 (3lo)) based authentication.
type BearerAuthTransport struct {
Expand Down
89 changes: 89 additions & 0 deletions jira_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jira

import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"math"
Expand All @@ -12,6 +13,8 @@ import (
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

const (
Expand Down Expand Up @@ -588,6 +591,92 @@ func TestBasicAuthTransport_transport(t *testing.T) {
}
}

func TestNetrcBasicAuthTransport(t *testing.T) {
username, password := "jirauser", "jirapass"

for _, test := range []struct {
Description string
NetrcPath string
ServerError string
ClientError string
}{
{
Description: "Missing netrc file",
NetrcPath: "test_data/netrc/nonexistent.netrc",
ClientError: "open test_data/netrc/nonexistent.netrc",
},
{
Description: "Machine exists",
NetrcPath: "test_data/netrc/machine.netrc",
},
{
Description: "Machine missing",
NetrcPath: "test_data/netrc/no_machine.netrc",
ClientError: "no credentials for machine",
},
{
Description: "Machine exists, but credentials incorrect",
NetrcPath: "test_data/netrc/wrong_creds.netrc",
ServerError: "request contained wrong basic auth password",
},
} {
setup()

var serverErr error
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok {
serverErr = errors.New("request does not contain basic auth credentials")
}
if u != username {
serverErr = fmt.Errorf("request contained wrong basic auth username: got %q, want %q", u, username)
}
if p != password {
serverErr = fmt.Errorf("request contained wrong basic auth password: got %q, want %q", p, password)
}
})

tp := NewNetrcBasicAuthTransport(test.NetrcPath)
client, _ := NewClient(tp.Client(), testServer.URL)
req, _ := client.NewRequest("GET", "/", nil)
_, clientErr := client.Do(req, nil)

if test.ServerError == "" {
assert.NoError(t, serverErr)
} else {
assert.ErrorContains(t, serverErr, test.ServerError)
}
if test.ClientError == "" {
assert.NoError(t, clientErr)
} else {
assert.ErrorContains(t, clientErr, test.ClientError)
}
if test.ServerError == "" && test.ClientError == "" {
u, err := tp.Username(req.URL.Hostname())
assert.Equal(t, username, u)
assert.NoError(t, err)
}

teardown()
}
}

func TestNetrcBasicAuthTransport_transport(t *testing.T) {
// default transport
tp := &NetrcBasicAuthTransport{}
if tp.transport() != defaultTransport {
t.Errorf("Expected defaultTransport to be used.")
}

// custom transport
tp = &NetrcBasicAuthTransport{
Transport: &http.Transport{},
}
if tp.transport() == defaultTransport {
t.Errorf("Expected custom transport to be used.")
}
}

// Test that the cookie in the transport is the cookie returned in the header
func TestCookieAuthTransport_SessionObject_Exists(t *testing.T) {
setup()
Expand Down
1 change: 1 addition & 0 deletions test_data/netrc/machine.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
machine 127.0.0.1 login jirauser password jirapass
2 changes: 2 additions & 0 deletions test_data/netrc/no_machine.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
machine fake.example login u password p
machine another.fake.example jirauser password jirapass
2 changes: 2 additions & 0 deletions test_data/netrc/wrong_creds.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
machine 127.0.0.1 login wronguser password wrongpass
machine 127.8.8.8 login jirauser password jirapass
Loading