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

Update NewClient handle credentials in the url #23

Merged
merged 12 commits into from
Oct 25, 2016
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ first. For more complete details see
* Updated internal function digestAuthHeader to return exported errors
(ErrWWWAuthenticateHeader*) rather than calling fmt.Errorf. This makes
it easier to test against externally and also fixes a lint issue too.
* Updated NewClient function to handle credentials in the url.

### 0.1.0

Expand Down
84 changes: 84 additions & 0 deletions gerrit.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,28 @@ var (
// ErrNoInstanceGiven is returned by NewClient in the event the
// gerritURL argument was blank.
ErrNoInstanceGiven = errors.New("No Gerrit instance given.")

// ErrUserProvidedWithoutPassword is returned by NewClientFromURL
// if a user name is provided without a password.
ErrUserProvidedWithoutPassword = errors.New("A username was provided without a password.")

// ErrAuthenticationFailed is returned by NewClientFromURL in the event the provided
// credentials didn't allow us to query account information using digest, basic or cookie
// auth.
ErrAuthenticationFailed = errors.New("Failed to authenticate using the provided credentials.")
)

// NewClient returns a new Gerrit API client. The gerritURL argument has to be the
// HTTP endpoint of the Gerrit instance, http://localhost:8080/ for example. If a nil
// httpClient is provided, http.DefaultClient will be used.
//
// The url may contain credentials, http://admin:secret@localhost:8081/ for
// example. These credentials may either be a user name and password or
// name and value as in the case of cookie based authentication. If the url contains
// credentials then this function will attempt to validate the credentials before
// returning the client. `ErrAuthenticationFailed` will be returned if the credentials
// cannot be validated. The process of validating the credentials is relatively simple and
// only requires that the provided user have permission to GET /a/accounts/self.
func NewClient(endpoint string, httpClient *http.Client) (*Client, error) {
if httpClient == nil {
httpClient = http.DefaultClient
Expand All @@ -75,6 +92,32 @@ func NewClient(endpoint string, httpClient *http.Client) (*Client, error) {
return nil, err
}

// Username and/or password provided as part of the url.

hasAuth := false
username := ""
password := ""
if baseURL.User != nil {
username = baseURL.User.Username()
parsedPassword, haspassword := baseURL.User.Password()

// Catches cases like http://user@localhost:8081/ where no password
// was at all. If a blank password is required
if !haspassword {
return nil, ErrUserProvidedWithoutPassword
}

password = parsedPassword

// Reconstruct the url but without the username and password.
baseURL, err = url.Parse(
fmt.Sprintf("%s://%s%s", baseURL.Scheme, baseURL.Host, baseURL.RequestURI()))
if err != nil {
return nil, err
}
hasAuth = true
}

c := &Client{
client: httpClient,
baseURL: baseURL,
Expand All @@ -89,9 +132,50 @@ func NewClient(endpoint string, httpClient *http.Client) (*Client, error) {
c.Projects = &ProjectsService{client: c}
c.EventsLog = &EventsLogService{client: c}

if hasAuth {
// Digest auth (first since that's the default auth type)
c.Authentication.SetDigestAuth(username, password)
if success, err := checkAuth(c); success || err != nil {
return c, err
}

// Basic auth
c.Authentication.SetBasicAuth(username, password)
if success, err := checkAuth(c); success || err != nil {
return c, err
}

// Cookie auth
c.Authentication.SetCookieAuth(username, password)
if success, err := checkAuth(c); success || err != nil {
return c, err
}

// Reset auth in case the consumer needs to do something special.
c.Authentication.ResetAuth()
return c, ErrAuthenticationFailed
}

return c, nil
}

// checkAuth is used by NewClientFromURL to check if the current credentials are
// valid. If the response is 401 Unauthorized then the error will be discarded.
func checkAuth(client *Client) (bool, error) {
_, response, err := client.Accounts.GetAccount("self")
switch err {
case ErrWWWAuthenticateHeaderMissing:
return false, nil
case ErrWWWAuthenticateHeaderNotDigest:
return false, nil
default:
if err != nil && response.StatusCode == http.StatusUnauthorized {
err = nil
}
return response.StatusCode == http.StatusOK, err
}
}

// NewRequest creates an API request.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
Expand Down
192 changes: 185 additions & 7 deletions gerrit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package gerrit_test

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"

"github.com/andygrunwald/go-gerrit"
Expand Down Expand Up @@ -47,6 +49,32 @@ func teardown() {
testServer.Close()
}

// makedigestheader takes the incoming request and produces a string
// which can be used for the WWW-Authenticate header.
func makedigestheader(request *http.Request) string {
return fmt.Sprintf(
`Digest realm="Gerrit Code Review", domain="http://%s/", qop="auth", nonce="fakevaluefortesting"`,
request.Host)
}

// writeresponse writes the requested value to the provided response writer and sets
// the http code
func writeresponse(t *testing.T, writer http.ResponseWriter, value interface{}, code int) {
writer.WriteHeader(code)

unmarshalled, err := json.Marshal(value)
if err != nil {
t.Error(err.Error())
return
}

data := []byte(`)]}'` + "\n" + string(unmarshalled))
if _, err := writer.Write(data); err != nil {
t.Error(err.Error())
return
}
}

func testMethod(t *testing.T, r *http.Request, want string) {
if got := r.Method; got != want {
t.Errorf("Request method: %v, want %v", got, want)
Expand Down Expand Up @@ -116,6 +144,163 @@ func TestNewClient_Services(t *testing.T) {
}
}

func TestNewClient_TestErrNoInstanceGiven(t *testing.T) {
_, err := gerrit.NewClient("", nil)
if err != gerrit.ErrNoInstanceGiven {
t.Error("Expected `ErrNoInstanceGiven`")
}
}

func TestNewClient_NoCredentials(t *testing.T) {
client, err := gerrit.NewClient("http://localhost/", nil)
if err != nil {
t.Errorf("Unexpected error: %s", err.Error())
}
if client.Authentication.HasAuth() {
t.Error("Expected HasAuth() to return false")
}
}

func TestNewClient_UsernameWithoutPassword(t *testing.T) {
_, err := gerrit.NewClient("http://foo@localhost/", nil)
if err != gerrit.ErrUserProvidedWithoutPassword {
t.Error("Expected ErrUserProvidedWithoutPassword")
}
}

func TestNewClient_AuthenticationFailed(t *testing.T) {
setup()
defer teardown()

testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) {
writeresponse(t, w, nil, http.StatusUnauthorized)
})

serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String())
client, err := gerrit.NewClient(serverURL, nil)
if err != gerrit.ErrAuthenticationFailed {
t.Error(err)
}
if client.Authentication.HasAuth() {
t.Error("Expected HasAuth() == false")
}
}

func TestNewClient_DigestAuth(t *testing.T) {
setup()
defer teardown()

account := gerrit.AccountInfo{
AccountID: 100000,
Name: "test",
Email: "test@localhost",
Username: "test"}
hits := 0

testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) {
hits++
switch hits {
case 1:
w.Header().Set("WWW-Authenticate", makedigestheader(r))
writeresponse(t, w, nil, http.StatusUnauthorized)
case 2:
// go-gerrit should set Authorization in response to a `WWW-Authenticate` header
if !strings.Contains(r.Header.Get("Authorization"), `username="admin"`) {
t.Error(`Missing username="admin"`)
}
writeresponse(t, w, account, http.StatusOK)
case 3:
t.Error("Did not expect another request")
}
})

serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String())
client, err := gerrit.NewClient(serverURL, nil)
if err != nil {
t.Error(err)
}
if !client.Authentication.HasDigestAuth() {
t.Error("Expected HasDigestAuth() == true")
}
}

func TestNewClient_BasicAuth(t *testing.T) {
setup()
defer teardown()

account := gerrit.AccountInfo{
AccountID: 100000,
Name: "test",
Email: "test@localhost",
Username: "test"}
hits := 0

testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) {
hits++
switch hits {
case 1:
writeresponse(t, w, nil, http.StatusUnauthorized)
case 2:
// The second request should be a basic auth request if the first request, which is for
// digest based auth, fails.
if !strings.HasPrefix(r.Header.Get("Authorization"), "Basic ") {
t.Error("Missing 'Basic ' prefix")
}
writeresponse(t, w, account, http.StatusOK)
case 3:
t.Error("Did not expect another request")
}
})

serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String())
client, err := gerrit.NewClient(serverURL, nil)
if err != nil {
t.Error(err)
}
if !client.Authentication.HasBasicAuth() {
t.Error("Expected HasBasicAuth() == true")
}
}

func TestNewClient_CookieAuth(t *testing.T) {
setup()
defer teardown()

account := gerrit.AccountInfo{
AccountID: 100000,
Name: "test",
Email: "test@localhost",
Username: "test"}
hits := 0

testMux.HandleFunc("/a/accounts/self", func(w http.ResponseWriter, r *http.Request) {
hits++
switch hits {
case 1:
writeresponse(t, w, nil, http.StatusUnauthorized)
case 2:
writeresponse(t, w, nil, http.StatusUnauthorized)
case 3:
if r.Header.Get("Cookie") != "admin=secret" {
t.Error("Expected cookie to equal 'admin=secret")
}

writeresponse(t, w, account, http.StatusOK)
case 4:
t.Error("Did not expect another request")
}
})

serverURL := fmt.Sprintf("http://admin:secret@%s/", testServer.Listener.Addr().String())
client, err := gerrit.NewClient(serverURL, nil)
if err != nil {
t.Error(err)
}
if !client.Authentication.HasCookieAuth() {
t.Error("Expected HasCookieAuth() == true")
}
}

func TestNewRequest(t *testing.T) {
c, err := gerrit.NewClient(testGerritInstanceURL, nil)
if err != nil {
Expand Down Expand Up @@ -288,10 +473,3 @@ func TestRemoveMagicPrefixLineDoesNothingWithoutPrefix(t *testing.T) {
}
}
}

func TestErrNoInstanceGiven(t *testing.T) {
_, err := gerrit.NewClient("", nil)
if err != gerrit.ErrNoInstanceGiven {
t.Error("Expected `ErrNoInstanceGiven`")
}
}