mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-02-09 13:36:58 +02:00
Merge pull request #112 from rbriski/rbriski/auth_change
Moving to RoundTripper for authentication
This commit is contained in:
commit
78dbcf28a8
95
README.md
95
README.md
@ -80,68 +80,49 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Authenticate with jira
|
||||
### Authentication
|
||||
|
||||
Some actions require an authenticated user.
|
||||
The `go-jira` library does not handle most authentication directly. Instead, authentication should be handled within
|
||||
an `http.Client`. That client can then be passed into the `NewClient` function when creating a jira client.
|
||||
|
||||
#### Authenticate with basic auth
|
||||
For convenience, capability for basic and cookie-based authentication is included in the main library.
|
||||
|
||||
Here is an example with basic auth authentication.
|
||||
#### Basic auth example
|
||||
|
||||
A more thorough, [runnable example](examples/basicauth/main.go) is provided in the examples directory.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/andygrunwald/go-jira"
|
||||
)
|
||||
|
||||
func main() {
|
||||
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
jiraClient.Authentication.SetBasicAuth("username", "password")
|
||||
|
||||
issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
tp := jira.BasicAuthTransport{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
client, err := jira.NewClient(tp.Client(), "https://my.jira.com")
|
||||
|
||||
u, _, err := client.User.Get("some_user")
|
||||
|
||||
fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)
|
||||
}
|
||||
```
|
||||
|
||||
#### Authenticate with session cookie
|
||||
|
||||
Here is an example with session cookie authentication.
|
||||
A more thorough, [runnable example](examples/cookieauth/main.go) is provided in the examples directory.
|
||||
|
||||
Note: The `AuthURL` is almost always going to have the path `/rest/auth/1/session`
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/andygrunwald/go-jira"
|
||||
)
|
||||
|
||||
func main() {
|
||||
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
tp := jira.CookieAuthTransport{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
AuthURL: "https://my.jira.com/rest/auth/1/session",
|
||||
}
|
||||
|
||||
res, err := jiraClient.Authentication.AcquireSessionCookie("username", "password")
|
||||
if err != nil || res == false {
|
||||
fmt.Printf("Result: %v\n", res)
|
||||
panic(err)
|
||||
}
|
||||
client, err := jira.NewClient(tp.Client(), "https://my.jira.com")
|
||||
u, _, err := client.User.Get("admin")
|
||||
|
||||
issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)
|
||||
}
|
||||
```
|
||||
|
||||
@ -164,14 +145,14 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
tp := jira.CookieAuthTransport{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
BaseURL: "https://my.jira.com",
|
||||
}
|
||||
|
||||
res, err := jiraClient.Authentication.AcquireSessionCookie("username", "password")
|
||||
if err != nil || res == false {
|
||||
fmt.Printf("Result: %v\n", res)
|
||||
jiraClient, err := jira.NewClient(tp.Client(), tp.BaseURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -217,7 +198,13 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
jiraClient, _ := jira.NewClient(nil, "https://jira.atlassian.com/")
|
||||
tp := jira.CookieAuthTransport{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
BaseURL: "https://my.jira.com",
|
||||
}
|
||||
|
||||
jiraClient, _ := jira.NewClient(tp.Client(), tp.BaseURL)
|
||||
req, _ := jiraClient.NewRequest("GET", "/rest/api/2/project", nil)
|
||||
|
||||
projects := new([]jira.Project)
|
||||
@ -271,6 +258,12 @@ If you are new to pull requests, checkout [Collaborating on projects using issue
|
||||
|
||||
For adding new dependencies, updating dependencies, and other operations, the [Daily Dep](https://golang.github.io/dep/docs/daily-dep.html) is a good place to start.
|
||||
|
||||
### Sandbox environment for testing
|
||||
|
||||
Jira offers sandbox test environments at http://go.atlassian.com/cloud-dev.
|
||||
|
||||
You can read more about them at https://developer.atlassian.com/blog/2016/04/cloud-ecosystem-dev-env/.
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License).
|
||||
|
@ -54,6 +54,8 @@ type Session struct {
|
||||
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
|
||||
//
|
||||
// Deprecated: Use CookieAuthTransport instead
|
||||
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
|
||||
apiEndpoint := "rest/auth/1/session"
|
||||
body := struct {
|
||||
@ -90,6 +92,8 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
|
||||
}
|
||||
|
||||
// SetBasicAuth sets username and password for the basic auth against the JIRA instance.
|
||||
//
|
||||
// Deprecated: Use BasicAuthTransport instead
|
||||
func (s *AuthenticationService) SetBasicAuth(username, password string) {
|
||||
s.username = username
|
||||
s.password = password
|
||||
@ -112,6 +116,9 @@ func (s *AuthenticationService) Authenticated() bool {
|
||||
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
|
||||
//
|
||||
// Deprecated: Use CookieAuthTransport to create base client. Logging out is as simple as not using the
|
||||
// client anymore
|
||||
func (s *AuthenticationService) Logout() error {
|
||||
if s.authType != authTypeSession || s.client.session == nil {
|
||||
return fmt.Errorf("No user is authenticated yet.")
|
||||
|
47
examples/basicauth/main.go
Normal file
47
examples/basicauth/main.go
Normal file
@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
jira "github.com/andygrunwald/go-jira"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Jira URL: ")
|
||||
jiraURL, _ := r.ReadString('\n')
|
||||
|
||||
fmt.Print("Jira Username: ")
|
||||
username, _ := r.ReadString('\n')
|
||||
|
||||
fmt.Print("Jira Password: ")
|
||||
bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin))
|
||||
password := string(bytePassword)
|
||||
|
||||
tp := jira.BasicAuthTransport{
|
||||
Username: strings.TrimSpace(username),
|
||||
Password: strings.TrimSpace(password),
|
||||
}
|
||||
|
||||
client, err := jira.NewClient(tp.Client(), strings.TrimSpace(jiraURL))
|
||||
if err != nil {
|
||||
fmt.Printf("\nerror: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
u, _, err := client.User.Get("admin")
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("\nerror: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("\nEmail: %v\nSuccess!\n", u.EmailAddress)
|
||||
|
||||
}
|
145
jira.go
145
jira.go
@ -8,8 +8,10 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// A Client manages communication with the JIRA API.
|
||||
@ -281,3 +283,146 @@ func (r *Response) populatePageValues(v interface{}) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
|
||||
// using HTTP Basic Authentication with the provided username and password.
|
||||
type BasicAuthTransport struct {
|
||||
Username string
|
||||
Password string
|
||||
|
||||
// Transport is the underlying HTTP transport to use when making requests.
|
||||
// It will default to http.DefaultTransport if nil.
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip implements the RoundTripper interface. We just add the
|
||||
// basic auth and return the RoundTripper for this transport type.
|
||||
func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req2 := cloneRequest(req) // per RoundTripper contract
|
||||
|
||||
req2.SetBasicAuth(t.Username, t.Password)
|
||||
return t.transport().RoundTrip(req2)
|
||||
}
|
||||
|
||||
// Client returns an *http.Client that makes requests that are authenticated
|
||||
// using HTTP Basic Authentication. 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 *BasicAuthTransport) Client() *http.Client {
|
||||
return &http.Client{Transport: t}
|
||||
}
|
||||
|
||||
func (t *BasicAuthTransport) transport() http.RoundTripper {
|
||||
if t.Transport != nil {
|
||||
return t.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// CookieAuthTransport is an http.RoundTripper that authenticates all requests
|
||||
// using Jira's cookie-based authentication.
|
||||
//
|
||||
// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
|
||||
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
|
||||
type CookieAuthTransport struct {
|
||||
Username string
|
||||
Password string
|
||||
AuthURL string
|
||||
|
||||
// SessionObject is the authenticated cookie string.s
|
||||
// It's passed in each call to prove the client is authenticated.
|
||||
SessionObject []*http.Cookie
|
||||
|
||||
// Transport is the underlying HTTP transport to use when making requests.
|
||||
// It will default to http.DefaultTransport if nil.
|
||||
Transport http.RoundTripper
|
||||
}
|
||||
|
||||
// RoundTrip adds the session object to the request.
|
||||
func (t *CookieAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if t.SessionObject == nil {
|
||||
err := t.setSessionObject()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cookieauth: no session object has been set")
|
||||
}
|
||||
}
|
||||
|
||||
req2 := cloneRequest(req) // per RoundTripper contract
|
||||
for _, cookie := range t.SessionObject {
|
||||
req2.AddCookie(cookie)
|
||||
}
|
||||
|
||||
return t.transport().RoundTrip(req2)
|
||||
}
|
||||
|
||||
// Client returns an *http.Client that makes requests that are authenticated
|
||||
// using cookie authentication
|
||||
func (t *CookieAuthTransport) Client() *http.Client {
|
||||
return &http.Client{Transport: t}
|
||||
}
|
||||
|
||||
// setSessionObject attempts to authenticate the user and set
|
||||
// the session object (e.g. cookie)
|
||||
func (t *CookieAuthTransport) setSessionObject() error {
|
||||
req, err := t.buildAuthRequest()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var authClient = &http.Client{
|
||||
Timeout: time.Second * 60,
|
||||
}
|
||||
resp, err := authClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.SessionObject = resp.Cookies()
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAuthRequest assembles the request to get the authenticated cookie
|
||||
func (t *CookieAuthTransport) buildAuthRequest() (*http.Request, error) {
|
||||
body := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}{
|
||||
t.Username,
|
||||
t.Password,
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
json.NewEncoder(b).Encode(body)
|
||||
|
||||
req, err := http.NewRequest("POST", t.AuthURL, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (t *CookieAuthTransport) transport() http.RoundTripper {
|
||||
if t.Transport != nil {
|
||||
return t.Transport
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
// shallow copy of the struct
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
152
jira_test.go
152
jira_test.go
@ -431,23 +431,147 @@ func TestClient_GetBaseURL_WithURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_Do_PagingInfoEmptyByDefault(t *testing.T) {
|
||||
c, _ := NewClient(nil, testJIRAInstanceURL)
|
||||
req, _ := c.NewRequest("GET", "/", nil)
|
||||
type foo struct {
|
||||
A string
|
||||
}
|
||||
body := new(foo)
|
||||
// REMOVED : This actually calls a live URL. It's not a unit test.
|
||||
// I'm also not really sure what it's testing.
|
||||
// func TestClient_Do_PagingInfoEmptyByDefault(t *testing.T) {
|
||||
// c, _ := NewClient(nil, testJIRAInstanceURL)
|
||||
// req, _ := c.NewRequest("GET", "/", nil)
|
||||
// t.Errorf("%v\n", req)
|
||||
// type foo struct {
|
||||
// A string
|
||||
// }
|
||||
// body := new(foo)
|
||||
|
||||
resp, _ := c.Do(req, body)
|
||||
// resp, _ := c.Do(req, body)
|
||||
|
||||
if resp.StartAt != 0 {
|
||||
t.Errorf("StartAt not equal to 0")
|
||||
// if resp.StartAt != 0 {
|
||||
// t.Errorf("StartAt not equal to 0")
|
||||
// }
|
||||
// if resp.MaxResults != 0 {
|
||||
// t.Errorf("StartAt not equal to 0")
|
||||
// }
|
||||
// if resp.Total != 0 {
|
||||
// t.Errorf("StartAt not equal to 0")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestBasicAuthTransport(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
username, password := "username", "password"
|
||||
|
||||
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
u, p, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
t.Errorf("request does not contain basic auth credentials")
|
||||
}
|
||||
if u != username {
|
||||
t.Errorf("request contained basic auth username %q, want %q", u, username)
|
||||
}
|
||||
if p != password {
|
||||
t.Errorf("request contained basic auth password %q, want %q", p, password)
|
||||
}
|
||||
})
|
||||
|
||||
tp := &BasicAuthTransport{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
if resp.MaxResults != 0 {
|
||||
t.Errorf("StartAt not equal to 0")
|
||||
|
||||
basicAuthClient, _ := NewClient(tp.Client(), testServer.URL)
|
||||
req, _ := basicAuthClient.NewRequest("GET", ".", nil)
|
||||
basicAuthClient.Do(req, nil)
|
||||
}
|
||||
|
||||
func TestBasicAuthTransport_transport(t *testing.T) {
|
||||
// default transport
|
||||
tp := &BasicAuthTransport{}
|
||||
if tp.transport() != http.DefaultTransport {
|
||||
t.Errorf("Expected http.DefaultTransport to be used.")
|
||||
}
|
||||
if resp.Total != 0 {
|
||||
t.Errorf("StartAt not equal to 0")
|
||||
|
||||
// custom transport
|
||||
tp = &BasicAuthTransport{
|
||||
Transport: &http.Transport{},
|
||||
}
|
||||
if tp.transport() == http.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()
|
||||
defer teardown()
|
||||
|
||||
testCookie := &http.Cookie{Name: "test", Value: "test"}
|
||||
|
||||
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
cookies := r.Cookies()
|
||||
|
||||
if len(cookies) < 1 {
|
||||
t.Errorf("No cookies set")
|
||||
}
|
||||
|
||||
if cookies[0].Name != testCookie.Name {
|
||||
t.Errorf("Cookie names don't match, expected %v, got %v", testCookie.Name, cookies[0].Name)
|
||||
}
|
||||
|
||||
if cookies[0].Value != testCookie.Value {
|
||||
t.Errorf("Cookie values don't match, expected %v, got %v", testCookie.Value, cookies[0].Value)
|
||||
}
|
||||
})
|
||||
|
||||
tp := &CookieAuthTransport{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
AuthURL: "https://some.jira.com/rest/auth/1/session",
|
||||
SessionObject: []*http.Cookie{testCookie},
|
||||
}
|
||||
|
||||
basicAuthClient, _ := NewClient(tp.Client(), testServer.URL)
|
||||
req, _ := basicAuthClient.NewRequest("GET", ".", nil)
|
||||
basicAuthClient.Do(req, nil)
|
||||
}
|
||||
|
||||
// Test that if no cookie is in the transport, it checks for a cookie
|
||||
func TestCookieAuthTransport_SessionObject_DoesNotExist(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testCookie := &http.Cookie{Name: "does_not_exist", Value: "does_not_exist"}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
http.SetCookie(w, testCookie)
|
||||
w.Write([]byte(`OK`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
cookies := r.Cookies()
|
||||
|
||||
if len(cookies) < 1 {
|
||||
t.Errorf("No cookies set")
|
||||
}
|
||||
|
||||
if cookies[0].Name != testCookie.Name {
|
||||
t.Errorf("Cookie names don't match, expected %v, got %v", testCookie.Name, cookies[0].Name)
|
||||
}
|
||||
|
||||
if cookies[0].Value != testCookie.Value {
|
||||
t.Errorf("Cookie values don't match, expected %v, got %v", testCookie.Value, cookies[0].Value)
|
||||
}
|
||||
})
|
||||
|
||||
tp := &CookieAuthTransport{
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
AuthURL: ts.URL,
|
||||
}
|
||||
|
||||
basicAuthClient, _ := NewClient(tp.Client(), testServer.URL)
|
||||
req, _ := basicAuthClient.NewRequest("GET", ".", nil)
|
||||
basicAuthClient.Do(req, nil)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user