2015-09-03 12:25:21 +02:00
package jira
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
2016-06-15 11:20:37 +02:00
"reflect"
2018-05-10 16:12:40 +02:00
"strings"
2018-02-25 02:29:17 +02:00
"time"
2016-06-15 11:20:37 +02:00
"github.com/google/go-querystring/query"
2018-02-25 02:29:17 +02:00
"github.com/pkg/errors"
2015-09-03 12:25:21 +02:00
)
// A Client manages communication with the JIRA API.
type Client struct {
// HTTP client used to communicate with the API.
client * http . Client
// Base URL for API requests.
baseURL * url . URL
// Session storage if the user authentificate with a Session cookie
session * Session
// Services used for talking to different parts of the JIRA API.
Authentication * AuthenticationService
Issue * IssueService
2016-05-29 18:42:38 +02:00
Project * ProjectService
2016-06-15 19:15:12 +02:00
Board * BoardService
2016-06-24 10:43:32 +02:00
Sprint * SprintService
2016-10-31 14:24:30 +02:00
User * UserService
2017-01-29 18:28:04 +02:00
Group * GroupService
2018-01-22 12:34:41 +02:00
Version * VersionService
2018-06-12 10:22:48 +02:00
Priority * PriorityService
2018-06-13 03:27:42 +02:00
Field * FieldService
2018-06-25 16:47:53 +02:00
Component * ComponentService
2018-06-25 17:48:53 +02:00
Resolution * ResolutionService
2018-06-26 09:41:46 +02:00
StatusCategory * StatusCategoryService
2015-09-03 12:25:21 +02:00
}
// NewClient returns a new JIRA API client.
// If a nil httpClient is provided, http.DefaultClient will be used.
2016-03-26 22:24:23 +02:00
// To use API methods which require authentication you can follow the preferred solution and
2015-09-03 12:25:21 +02:00
// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
// As an alternative you can use Session Cookie based authentication provided by this package as well.
// See https://docs.atlassian.com/jira/REST/latest/#authentication
// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash.
func NewClient ( httpClient * http . Client , baseURL string ) ( * Client , error ) {
if httpClient == nil {
httpClient = http . DefaultClient
}
2018-05-09 02:43:24 +02:00
// ensure the baseURL contains a trailing slash so that all paths are preserved in later calls
if ! strings . HasSuffix ( baseURL , "/" ) {
baseURL += "/"
}
2015-09-03 12:25:21 +02:00
parsedBaseURL , err := url . Parse ( baseURL )
if err != nil {
return nil , err
}
c := & Client {
client : httpClient ,
baseURL : parsedBaseURL ,
}
c . Authentication = & AuthenticationService { client : c }
c . Issue = & IssueService { client : c }
2016-05-29 18:42:38 +02:00
c . Project = & ProjectService { client : c }
2016-06-15 19:08:15 +02:00
c . Board = & BoardService { client : c }
2016-06-24 10:43:32 +02:00
c . Sprint = & SprintService { client : c }
2016-10-31 14:24:30 +02:00
c . User = & UserService { client : c }
2017-01-29 18:28:04 +02:00
c . Group = & GroupService { client : c }
2018-01-22 12:34:41 +02:00
c . Version = & VersionService { client : c }
2018-06-12 10:22:48 +02:00
c . Priority = & PriorityService { client : c }
2018-06-13 03:27:42 +02:00
c . Field = & FieldService { client : c }
2018-06-25 16:47:53 +02:00
c . Component = & ComponentService { client : c }
2018-06-25 17:48:53 +02:00
c . Resolution = & ResolutionService { client : c }
2018-06-26 09:41:46 +02:00
c . StatusCategory = & StatusCategoryService { client : c }
2015-09-03 12:25:21 +02:00
return c , nil
}
2016-10-10 09:46:21 +02:00
// NewRawRequest 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.
// Allows using an optional native io.Reader for sourcing the request body.
func ( c * Client ) NewRawRequest ( method , urlStr string , body io . Reader ) ( * http . Request , error ) {
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
2018-05-09 02:43:24 +02:00
// Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
2018-05-10 16:12:40 +02:00
rel . Path = strings . TrimLeft ( rel . Path , "/" )
2016-10-10 09:46:21 +02:00
u := c . baseURL . ResolveReference ( rel )
req , err := http . NewRequest ( method , u . String ( ) , body )
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
2017-02-09 00:37:57 +02:00
// Set authentication information
if c . Authentication . authType == authTypeSession {
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
} else if c . Authentication . authType == authTypeBasic {
// Set basic auth information
if c . Authentication . username != "" {
req . SetBasicAuth ( c . Authentication . username , c . Authentication . password )
2016-10-10 09:46:21 +02:00
}
}
return req , nil
}
2015-09-03 12:25:21 +02:00
// 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.
// If specified, the value pointed to by body is JSON encoded and included as the request body.
func ( c * Client ) NewRequest ( method , urlStr string , body interface { } ) ( * http . Request , error ) {
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
2018-05-09 02:43:24 +02:00
// Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
2018-05-10 16:12:40 +02:00
rel . Path = strings . TrimLeft ( rel . Path , "/" )
2015-09-03 12:25:21 +02:00
u := c . baseURL . ResolveReference ( rel )
var buf io . ReadWriter
if body != nil {
buf = new ( bytes . Buffer )
2016-10-23 14:51:29 +02:00
err = json . NewEncoder ( buf ) . Encode ( body )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
}
req , err := http . NewRequest ( method , u . String ( ) , buf )
if err != nil {
return nil , err
}
req . Header . Set ( "Content-Type" , "application/json" )
2017-02-09 00:37:57 +02:00
// Set authentication information
if c . Authentication . authType == authTypeSession {
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
} else if c . Authentication . authType == authTypeBasic {
// Set basic auth information
if c . Authentication . username != "" {
req . SetBasicAuth ( c . Authentication . username , c . Authentication . password )
2016-05-27 12:01:54 +02:00
}
2015-09-03 12:25:21 +02:00
}
return req , nil
}
2016-06-15 11:20:37 +02:00
// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func addOptions ( s string , opt interface { } ) ( string , error ) {
v := reflect . ValueOf ( opt )
if v . Kind ( ) == reflect . Ptr && v . IsNil ( ) {
return s , nil
}
u , err := url . Parse ( s )
if err != nil {
return s , err
}
qs , err := query . Values ( opt )
if err != nil {
return s , err
}
u . RawQuery = qs . Encode ( )
return u . String ( ) , nil
}
2016-05-27 14:14:09 +02:00
// NewMultiPartRequest creates an API request including a multi-part file.
2016-05-19 23:11:21 +02:00
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
2016-05-27 14:14:09 +02:00
// If specified, the value pointed to by buf is a multipart form.
2016-05-19 23:11:21 +02:00
func ( c * Client ) NewMultiPartRequest ( method , urlStr string , buf * bytes . Buffer ) ( * http . Request , error ) {
rel , err := url . Parse ( urlStr )
if err != nil {
return nil , err
}
2018-05-09 02:43:24 +02:00
// Relative URLs should be specified without a preceding slash since baseURL will have the trailing slash
2018-05-10 16:12:40 +02:00
rel . Path = strings . TrimLeft ( rel . Path , "/" )
2016-05-19 23:11:21 +02:00
u := c . baseURL . ResolveReference ( rel )
req , err := http . NewRequest ( method , u . String ( ) , buf )
if err != nil {
return nil , err
}
// Set required headers
req . Header . Set ( "X-Atlassian-Token" , "nocheck" )
2017-02-09 00:37:57 +02:00
// Set authentication information
if c . Authentication . authType == authTypeSession {
// Set session cookie if there is one
if c . session != nil {
for _ , cookie := range c . session . Cookies {
req . AddCookie ( cookie )
}
}
} else if c . Authentication . authType == authTypeBasic {
// Set basic auth information
if c . Authentication . username != "" {
req . SetBasicAuth ( c . Authentication . username , c . Authentication . password )
2016-07-27 12:21:09 +02:00
}
2016-05-19 23:11:21 +02:00
}
return req , nil
}
2015-09-03 12:25:21 +02:00
// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
2016-06-15 17:09:13 +02:00
func ( c * Client ) Do ( req * http . Request , v interface { } ) ( * Response , error ) {
httpResp , err := c . client . Do ( req )
2015-09-03 12:25:21 +02:00
if err != nil {
return nil , err
}
2016-06-15 17:09:13 +02:00
err = CheckResponse ( httpResp )
2015-09-03 12:25:21 +02:00
if err != nil {
// Even though there was an error, we still return the response
// in case the caller wants to inspect it further
2016-06-15 17:09:13 +02:00
return newResponse ( httpResp , nil ) , err
2015-09-03 12:25:21 +02:00
}
if v != nil {
2016-05-24 07:48:04 +02:00
// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
2016-06-15 17:09:13 +02:00
defer httpResp . Body . Close ( )
err = json . NewDecoder ( httpResp . Body ) . Decode ( v )
2015-09-03 12:25:21 +02:00
}
2016-06-15 17:09:13 +02:00
resp := newResponse ( httpResp , v )
2015-09-03 12:25:21 +02:00
return resp , err
}
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if it has a status code outside the 200 range.
2016-05-28 20:15:17 +02:00
// The caller is responsible to analyze the response body.
// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes).
2015-09-03 12:25:21 +02:00
func CheckResponse ( r * http . Response ) error {
if c := r . StatusCode ; 200 <= c && c <= 299 {
return nil
}
2016-05-28 20:15:17 +02:00
err := fmt . Errorf ( "Request failed. Please analyze the request body for more details. Status code: %d" , r . StatusCode )
return err
2015-09-03 12:25:21 +02:00
}
2016-06-03 18:18:01 +02:00
// GetBaseURL will return you the Base URL.
// This is the same URL as in the NewClient constructor
func ( c * Client ) GetBaseURL ( ) url . URL {
return * c . baseURL
}
2016-06-15 17:09:13 +02:00
// Response represents JIRA API response. It wraps http.Response returned from
// API and provides information about paging.
type Response struct {
* http . Response
StartAt int
MaxResults int
Total int
}
func newResponse ( r * http . Response , v interface { } ) * Response {
resp := & Response { Response : r }
resp . populatePageValues ( v )
return resp
}
// Sets paging values if response json was parsed to searchResult type
// (can be extended with other types if they also need paging info)
func ( r * Response ) populatePageValues ( v interface { } ) {
switch value := v . ( type ) {
case * searchResult :
r . StartAt = value . StartAt
r . MaxResults = value . MaxResults
r . Total = value . Total
2018-03-16 12:01:13 +02:00
case * groupMembersResult :
r . StartAt = value . StartAt
r . MaxResults = value . MaxResults
r . Total = value . Total
2016-06-15 17:09:13 +02:00
}
return
}
2018-02-24 21:27:46 +02:00
// 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 ) {
2018-02-25 02:29:17 +02:00
req2 := cloneRequest ( req ) // per RoundTripper contract
req2 . SetBasicAuth ( t . Username , t . Password )
return t . transport ( ) . RoundTrip ( req2 )
2018-02-24 21:27:46 +02:00
}
// 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
}
2018-02-25 02:29:17 +02:00
// 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
2018-02-28 08:50:02 +02:00
AuthURL string
2018-02-25 02:29:17 +02:00
// SessionObject is the authenticated cookie string.s
// It's passed in each call to prove the client is authenticated.
2018-02-28 08:50:02 +02:00
SessionObject [ ] * http . Cookie
2018-02-25 02:29:17 +02:00
// 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 ) {
2018-02-28 08:50:02 +02:00
if t . SessionObject == nil {
2018-02-25 02:29:17 +02:00
err := t . setSessionObject ( )
if err != nil {
return nil , errors . Wrap ( err , "cookieauth: no session object has been set" )
}
}
req2 := cloneRequest ( req ) // per RoundTripper contract
2018-02-28 08:50:02 +02:00
for _ , cookie := range t . SessionObject {
2018-09-05 23:23:34 +02:00
// Don't add an empty value cookie to the request
if cookie . Value != "" {
req2 . AddCookie ( cookie )
}
2018-02-25 02:29:17 +02:00
}
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 {
2018-02-28 08:50:02 +02:00
req , err := t . buildAuthRequest ( )
2018-02-25 02:29:17 +02:00
if err != nil {
return err
}
var authClient = & http . Client {
Timeout : time . Second * 60 ,
}
resp , err := authClient . Do ( req )
if err != nil {
return err
}
2018-02-28 08:50:02 +02:00
t . SessionObject = resp . Cookies ( )
2018-02-25 02:29:17 +02:00
return nil
}
// getAuthRequest assembles the request to get the authenticated cookie
2018-02-28 08:50:02 +02:00
func ( t * CookieAuthTransport ) buildAuthRequest ( ) ( * http . Request , error ) {
2018-02-25 02:29:17 +02:00
body := struct {
Username string ` json:"username" `
Password string ` json:"password" `
} {
t . Username ,
t . Password ,
}
b := new ( bytes . Buffer )
json . NewEncoder ( b ) . Encode ( body )
2018-02-28 08:50:02 +02:00
req , err := http . NewRequest ( "POST" , t . AuthURL , b )
2018-02-25 02:29:17 +02:00
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
}