From aceeaadaa962eeabeaf599799497101a2d299cf7 Mon Sep 17 00:00:00 2001 From: Maksym Novozhylov Date: Tue, 20 Jun 2023 08:46:13 +0200 Subject: [PATCH 1/5] feat(grand): add support for Client Credentials Grant --- .github/workflows/build.yml | 2 +- CHANGES.md | 3 + api/client.go | 444 +++++++++++++++++++----------------- api/config.go | 221 +++++++++--------- example/example.go | 243 ++++++++++---------- go.mod | 18 ++ go.sum | 33 +++ 7 files changed, 532 insertions(+), 432 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3e9966d..71f8c31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - go: [ '1.14', '1.15' ] + go: [ '1.14', '1.15', '1.19' ] steps: diff --git a/CHANGES.md b/CHANGES.md index 3bbcff1..b549884 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +## 2.2.0 +* Add support for Client Credentials Grant + ## 2.1.5 * Add GraphQL support diff --git a/api/client.go b/api/client.go index 04e8c0c..9c90352 100644 --- a/api/client.go +++ b/api/client.go @@ -15,314 +15,340 @@ package api import ( - "log" - "strings" - "fmt" - "bytes" - "context" - "net/http" - "net/url" - "io/ioutil" - "encoding/json" - - "golang.org/x/oauth2" + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "strings" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" ) // Define end points const ( - // oauth2 and api flow - BaseHost = "https://www.upwork.com/" - DefaultEpoint = "api" - GqlEndpoint = "https://api.upwork.com/graphql" - AuthorizationEP = BaseHost + "ab/account-security/oauth2/authorize" - AccessTokenEP = BaseHost + DefaultEpoint + "/v3/oauth2/token" - DataFormat = "json" - OverloadParam = "http_method" - UPWORK_LIBRARY_USER_AGENT = "Github Upwork API Golang Library" - - // response types - ByteResponse = "[]byte" - ErrorResponse = "error" + // oauth2 and api flow + BaseHost = "https://stage.upwork.com/" + DefaultEpoint = "api" + GqlEndpoint = "https://api.upwork.com/graphql" + AuthorizationEP = BaseHost + "ab/account-security/oauth2/authorize" + AccessTokenEP = BaseHost + DefaultEpoint + "/v3/oauth2/token" + DataFormat = "json" + OverloadParam = "http_method" + UPWORK_LIBRARY_USER_AGENT = "Github Upwork API Golang Library" + + // response types + ByteResponse = "[]byte" + ErrorResponse = "error" ) // Api client type ApiClient struct { - // oauth2 - oconf *oauth2.Config - token *oauth2.Token - oclient *http.Client - config *Config - - // refresh token notify function - rnfunc TokenNotifyFunc - - // client - ep string - respType string - sendPostAsJson bool - hasCustomHttpClient bool + // oauth2 + oconf *oauth2.Config // Code Authorization Grant + cconf *clientcredentials.Config // Client Credentials Grant + token *oauth2.Token + oclient *http.Client + config *Config + + // refresh token notify function + rnfunc TokenNotifyFunc + + // client + ep string + respType string + sendPostAsJson bool + hasCustomHttpClient bool } // TokenNotifyFunc is a function that accepts an oauth2 Token upon refresh, and // returns an error if it should not be used. type TokenNotifyFunc func(*oauth2.Token) error +//type getApiClientConfig func() + // NotifyingTokenSource is an oauth2.TokenSource that calls a function when a // new token is obtained. type NotifyingTokenSource struct { - f TokenNotifyFunc - src oauth2.TokenSource + f TokenNotifyFunc + src oauth2.TokenSource } // Setup client using specific config func Setup(config *Config) (client ApiClient) { - var c ApiClient - - c.config = config - - c.oconf = &oauth2.Config{ - ClientID: config.ClientId, - ClientSecret: config.ClientSecret, - RedirectURL: config.RedirectUri, - Endpoint: oauth2.Endpoint{ - TokenURL: AccessTokenEP, - AuthURL: AuthorizationEP, - }, - } - - c.hasCustomHttpClient = config.HasCustomHttpClient - - // Force setup of client_id as a parameter - oauth2.RegisterBrokenAuthHeaderProvider(BaseHost) - - c.token = new(oauth2.Token) - c.token.TokenType = "Bearer" - c.token.AccessToken = config.AccessToken - c.token.RefreshToken = config.RefreshToken - c.token.Expiry = config.ExpiresAt - - c.SetApiResponseType(ByteResponse) - c.SetPostAsJson(false) // send by default using PostForm - - return c + var c ApiClient + + c.config = config + + if config.GrantType == "client_credentials" { + c.cconf = &clientcredentials.Config{ + ClientID: config.ClientId, + ClientSecret: config.ClientSecret, + TokenURL: AccessTokenEP, + } + } else { + c.oconf = &oauth2.Config{ + ClientID: config.ClientId, + ClientSecret: config.ClientSecret, + RedirectURL: config.RedirectUri, + Endpoint: oauth2.Endpoint{ + TokenURL: AccessTokenEP, + AuthURL: AuthorizationEP, + }, + } + } + + c.hasCustomHttpClient = config.HasCustomHttpClient + + // Force setup of client_id as a parameter + oauth2.RegisterBrokenAuthHeaderProvider(BaseHost) + + c.token = new(oauth2.Token) + c.token.TokenType = "Bearer" + c.token.AccessToken = config.AccessToken + c.token.RefreshToken = config.RefreshToken + c.token.Expiry = config.ExpiresAt + + c.SetApiResponseType(ByteResponse) + c.SetPostAsJson(false) // send by default using PostForm + + return c } // NewNotifyingTokenSource creates a NotifyingTokenSource from an underlying src // and calls f when a new token is obtained. func NewNotifyingTokenSource(src oauth2.TokenSource, f TokenNotifyFunc) *NotifyingTokenSource { - return &NotifyingTokenSource{f: f, src: src} + return &NotifyingTokenSource{f: f, src: src} } // Token fetches a new token from the underlying source. func (s *NotifyingTokenSource) Token() (*oauth2.Token, error) { - t, err := s.src.Token() - if err != nil { - return nil, err - } - if s.f == nil { - return t, nil - } - return t, s.f(t) + t, err := s.src.Token() + if err != nil { + return nil, err + } + if s.f == nil { + return t, nil + } + return t, s.f(t) } // Set refresh token notify function -func (c *ApiClient) SetRefreshTokenNotifyFunc(f TokenNotifyFunc) () { - c.rnfunc = f +func (c *ApiClient) SetRefreshTokenNotifyFunc(f TokenNotifyFunc) { + c.rnfunc = f } // Set request type for non-Get requests func (c *ApiClient) SetPostAsJson(t bool) { - c.sendPostAsJson = t; + c.sendPostAsJson = t } // Set API response type func (c *ApiClient) SetApiResponseType(t string) { - c.respType = t; + c.respType = t } // Set entry point, e.g requested from a router func (c *ApiClient) SetEntryPoint(ep string) { - c.ep = ep + c.ep = ep } -// Receive an authorization URL +// Receive an authorization URL in Code Authorization Grant func (c *ApiClient) GetAuthorizationUrl(stateString string) (authzUrl string) { - url := c.oconf.AuthCodeURL(stateString) - if url == "" { - log.Fatal("Can not get authorization URL using OAuth2 library") - } + url := c.oconf.AuthCodeURL(stateString) + if url == "" { + log.Fatal("Can not get authorization URL using OAuth2 library") + } - return url + return url } // Get access token using a specific authorization code -func (c *ApiClient) GetToken(ctx context.Context, authzCode string) (*oauth2.Token) { - ctx = c.config.SetOwnHttpClient(ctx) +func (c *ApiClient) GetToken(ctx context.Context, authzCode string) *oauth2.Token { + var ( + accessToken *oauth2.Token + err error + ) + + ctx = c.config.SetOwnHttpClient(ctx) + + if c.config.GrantType == "client_credentials" { + accessToken, err = c.cconf.Token(ctx) + } else { + accessToken, err = c.oconf.Exchange(ctx, strings.Trim(authzCode, "\n")) + } - accessToken, err := c.oconf.Exchange(ctx, strings.Trim(authzCode, "\n")) - if err != nil { - log.Fatal(err) - } + if err != nil { + log.Fatal(err) + } - c.token = accessToken - c.setupOauth2Client(ctx) + c.token = accessToken + c.setupOauth2Client(ctx) - return accessToken + return accessToken } // Check if client contains already a access/refresh token pair -func (c *ApiClient) HasAccessToken(ctx context.Context) (bool) { - has := (c.token != nil && (c.token.AccessToken != "" && c.token.RefreshToken != "")) - if has { - c.setupOauth2Client(ctx) - } - return has +func (c *ApiClient) HasAccessToken(ctx context.Context) bool { + has := (c.token != nil && (c.token.AccessToken != "" && c.token.RefreshToken != "")) + if has { + c.setupOauth2Client(ctx) + } + return has } // GET method for client func (c *ApiClient) Get(uri string, params map[string]string) (r *http.Response, re interface{}) { - // parameters must be encoded according to RFC 3986 - // hmmm, it seems to be the easiest trick? - qstr := "" - if params != nil { - for k, v := range params { - qstr += fmt.Sprintf("%s=%s&", k, v) - } - qstr = qstr[0:len(qstr)-1] - } - u := &url.URL{Path: qstr} - - // https://github.com/mrjones/oauth/issues/34 - encQuery := strings.Replace(u.String(), ";", "%3B", -1) - encQuery = strings.Replace(encQuery, "./", "?", 1) // see URL.String method to understand when "./" is returned - - // non-empty string may miss "?" - if encQuery !="" && encQuery[:1] != "?" { - encQuery = "?" + encQuery - } - - response, err := c.oclient.Get(formatUri(uri, c.ep) + encQuery) - - return c.getTypedResponse(response, err) + // parameters must be encoded according to RFC 3986 + // hmmm, it seems to be the easiest trick? + qstr := "" + if params != nil { + for k, v := range params { + qstr += fmt.Sprintf("%s=%s&", k, v) + } + qstr = qstr[0 : len(qstr)-1] + } + u := &url.URL{Path: qstr} + + // https://github.com/mrjones/oauth/issues/34 + encQuery := strings.Replace(u.String(), ";", "%3B", -1) + encQuery = strings.Replace(encQuery, "./", "?", 1) // see URL.String method to understand when "./" is returned + + // non-empty string may miss "?" + if encQuery != "" && encQuery[:1] != "?" { + encQuery = "?" + encQuery + } + + response, err := c.oclient.Get(formatUri(uri, c.ep) + encQuery) + + return c.getTypedResponse(response, err) } // POST method for client func (c *ApiClient) Post(uri string, params map[string]string) (r *http.Response, re interface{}) { - return c.sendPostRequest(uri, params) + return c.sendPostRequest(uri, params) } // PUT method for client func (c *ApiClient) Put(uri string, params map[string]string) (r *http.Response, re interface{}) { - return c.sendPostRequest(uri, addOverloadParam(params, "put")) + return c.sendPostRequest(uri, addOverloadParam(params, "put")) } // DELETE method for client func (c *ApiClient) Delete(uri string, params map[string]string) (r *http.Response, re interface{}) { - return c.sendPostRequest(uri, addOverloadParam(params, "delete")) + return c.sendPostRequest(uri, addOverloadParam(params, "delete")) } // setup/save authorized oauth2 client, based on received or provided access/refresh token pair func (c *ApiClient) setupOauth2Client(ctx context.Context) { - if (c.hasCustomHttpClient == false) { - ctx = c.config.SetOwnHttpClient(ctx) - } - - if c.rnfunc != nil { - // setup notifier for token-refresh workflow - https://github.com/golang/oauth2/issues/84 - realSource := c.oconf.TokenSource(ctx, c.token) - notifyingSrc := NewNotifyingTokenSource(realSource, c.rnfunc) - notifyingWithInitialSrc := oauth2.ReuseTokenSource(c.token, notifyingSrc) - // setup authorized oauth2 client - c.oclient = oauth2.NewClient(ctx, notifyingWithInitialSrc) - } else { - c.oclient = c.oconf.Client(ctx, c.token) - } + if c.hasCustomHttpClient == false { + ctx = c.config.SetOwnHttpClient(ctx) + } + + if c.rnfunc != nil && c.config.GrantType != "client_credentials" { + // setup notifier for token-refresh workflow - https://github.com/golang/oauth2/issues/84 + realSource := c.oconf.TokenSource(ctx, c.token) + notifyingSrc := NewNotifyingTokenSource(realSource, c.rnfunc) + notifyingWithInitialSrc := oauth2.ReuseTokenSource(c.token, notifyingSrc) + // setup authorized oauth2 client + c.oclient = oauth2.NewClient(ctx, notifyingWithInitialSrc) + } else { + if c.config.GrantType == "client_credentials" { + c.oclient = c.cconf.Client(ctx) + } else { + c.oclient = c.oconf.Client(ctx, c.token) + } + } } // setup X-Upwork-API-TenantId header func (c *ApiClient) SetOrgUidHeader(ctx context.Context, tenantId string) { - c.config.SetOrgUidHeader(tenantId) - c.setupOauth2Client(ctx) + c.config.SetOrgUidHeader(tenantId) + c.setupOauth2Client(ctx) } // run post/put/delete requests func (c *ApiClient) sendPostRequest(uri string, params map[string]string) (*http.Response, interface{}) { - var ( - response *http.Response - err error - ) - - if c.ep == "graphql" { - jsonStr, _ := json.Marshal(params) // params contain json data in this case - response, err = c.oclient.Post(GqlEndpoint, "application/json", bytes.NewBuffer(jsonStr)) - } else if c.sendPostAsJson == true { - // old style for backward compatibility with the old library - var jsonStr = []byte("{}") - if params != nil { - str := "" - for k, v := range params { - str += fmt.Sprintf("\"%s\": \"%s\",", k, v) - } - jsonStr = []byte(fmt.Sprintf("{%s}", str[0:len(str)-1])) - } - - response, err = c.oclient.Post(formatUri(uri, c.ep), "application/json", bytes.NewBuffer(jsonStr)) - } else { - // prefered - urlValues := url.Values{} - if params != nil { - for k, v := range params { - urlValues.Add(k, v) - } - } - - response, err = c.oclient.PostForm(formatUri(uri, c.ep), urlValues) - } - - return c.getTypedResponse(response, err) + var ( + response *http.Response + err error + ) + + if c.ep == "graphql" { + jsonStr, _ := json.Marshal(params) // params contain json data in this case + response, err = c.oclient.Post(GqlEndpoint, "application/json", bytes.NewBuffer(jsonStr)) + } else if c.sendPostAsJson == true { + // old style for backward compatibility with the old library + var jsonStr = []byte("{}") + if params != nil { + str := "" + for k, v := range params { + str += fmt.Sprintf("\"%s\": \"%s\",", k, v) + } + jsonStr = []byte(fmt.Sprintf("{%s}", str[0:len(str)-1])) + } + + response, err = c.oclient.Post(formatUri(uri, c.ep), "application/json", bytes.NewBuffer(jsonStr)) + } else { + // prefered + urlValues := url.Values{} + if params != nil { + for k, v := range params { + urlValues.Add(k, v) + } + } + + response, err = c.oclient.PostForm(formatUri(uri, c.ep), urlValues) + } + + return c.getTypedResponse(response, err) } // return proper response type func (c *ApiClient) getTypedResponse(resp *http.Response, re error) (*http.Response, interface{}) { - if c.respType == ByteResponse { - r, b := formatResponse(resp, re) - return r, b.([]byte) - } else { - return resp, re - } + if c.respType == ByteResponse { + r, b := formatResponse(resp, re) + return r, b.([]byte) + } else { + return resp, re + } } // Check and format (preparate a byte body) http response routine func formatResponse(resp *http.Response, err error) (*http.Response, interface{}) { - if err != nil { - log.Fatal("Can not execute the request, " + err.Error()) - } - - defer resp.Body.Close() - if resp.StatusCode != 200 { - // do not exit, it can be a normal response - // it's up to client/requester's side decide what to do - } - // read json http response - jsonDataFromHttp, _ := ioutil.ReadAll(resp.Body) - - return resp, jsonDataFromHttp + if err != nil { + log.Fatal("Can not execute the request, " + err.Error()) + } + + defer resp.Body.Close() + if resp.StatusCode != 200 { + // do not exit, it can be a normal response + // it's up to client/requester's side decide what to do + } + // read json http response + jsonDataFromHttp, _ := ioutil.ReadAll(resp.Body) + + return resp, jsonDataFromHttp } // Create a path to a specific resource -func formatUri(uri string, ep string) (string) { - format := "" - if ep == DefaultEpoint { - format += "." + DataFormat - } - return BaseHost + ep + uri + format +func formatUri(uri string, ep string) string { + format := "" + if ep == DefaultEpoint { + format += "." + DataFormat + } + return BaseHost + ep + uri + format } // add overload parameter to the map of parameters func addOverloadParam(params map[string]string, op string) map[string]string { - if params == nil { - params = make(map[string]string) - } - params[OverloadParam] = op - return params + if params == nil { + params = make(map[string]string) + } + params[OverloadParam] = op + return params } diff --git a/api/config.go b/api/config.go index 3348bfa..f45e549 100644 --- a/api/config.go +++ b/api/config.go @@ -15,153 +15,164 @@ package api import ( - "log" - "fmt" - "time" - "context" - "encoding/json" - "io/ioutil" - "net/http" - - "golang.org/x/oauth2" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "time" + + "golang.org/x/oauth2" ) const ( - TIMEFORMAT = "2006-01-02T15:04:05.000Z" // NOTE: time.RFC3339 does not work for unclear reason? + TIMEFORMAT = "2006-01-02T15:04:05.000Z" // NOTE: time.RFC3339 does not work for unclear reason? ) type HeadersTransport struct { - rt http.RoundTripper - uaHeader string - xTenantIdHeader string + rt http.RoundTripper + uaHeader string + xTenantIdHeader string } // Config type Config struct { - ClientId string - ClientSecret string - AccessToken string - RefreshToken string - RedirectUri string - ExpiresIn string - ExpiresAt time.Time - State string - Debug bool - HasCustomHttpClient bool - TenantIdHeader string // X-Upwork-API-TenantId required for GraphQL requests + ClientId string + ClientSecret string + AccessToken string + RefreshToken string + RedirectUri string + ExpiresIn string + ExpiresAt time.Time + State string + GrantType string + Debug bool + HasCustomHttpClient bool + TenantIdHeader string // X-Upwork-API-TenantId required for GraphQL requests } // List of required configuration keys -var requiredKeys = [3]string{"client_id", "client_secret", "redirect_uri"} +var requiredKeys = [2]string{"client_id", "client_secret"} // Create a new config func NewConfig(data map[string]string) (settings *Config) { - cfg := &Config{ - ClientId: data["client_id"], - ClientSecret: data["client_secret"], - RedirectUri: data["redirect_uri"], - } - - // save access token if defined - if val, ok := data["access_token"]; ok { - cfg.AccessToken = val - } - - // save refresh token if defined - if val, ok := data["refresh_token"]; ok { - cfg.RefreshToken = val - } - - // save expires_in if defined - if val, ok := data["expires_in"]; ok { - cfg.ExpiresIn = val - } - - // save expiresat if defined - if val, ok := data["expires_at"]; ok { - cfg.ExpiresAt, _ = time.Parse(TIMEFORMAT, val) - } - - // save state if defined - if val, ok := data["state"]; ok { - cfg.State = val - } - - // save debug flag if defined - if debug, ok := data["debug"]; ok && debug == "on" { - cfg.Debug = true - } - - return cfg + cfg := &Config{ + ClientId: data["client_id"], + ClientSecret: data["client_secret"], + RedirectUri: data["redirect_uri"], + } + + // save access token if defined + if val, ok := data["access_token"]; ok { + cfg.AccessToken = val + } + + // save refresh token if defined + if val, ok := data["refresh_token"]; ok { + cfg.RefreshToken = val + } + + // save expires_in if defined + if val, ok := data["expires_in"]; ok { + cfg.ExpiresIn = val + } + + // save expiresat if defined + if val, ok := data["expires_at"]; ok { + cfg.ExpiresAt, _ = time.Parse(TIMEFORMAT, val) + } + + // save state if defined + if val, ok := data["state"]; ok { + cfg.State = val + } + + // save grant_type if defined + if val, ok := data["grant_type"]; ok { + cfg.GrantType = val + } + + // save debug flag if defined + if debug, ok := data["debug"]; ok && debug == "on" { + cfg.Debug = true + } + + return cfg } // Read a specific configuration (json) file func ReadConfig(fn string) (settings *Config) { - // read from config file if exists - b, err := ioutil.ReadFile(fn) - if err != nil { - log.Fatal("config file: ", err) - } - - // parse json config - var data map[string]interface{} - if err := json.Unmarshal(b, &data); err != nil { - log.Fatal("config file: ", err) - } - - // test required properties - for _, v := range requiredKeys { - _, ok := data[v] - if !ok { - log.Fatal("config file: " + v + " is missing in " + fn) - } - } - - // convert - config := make(map[string]string) - for k, v := range data { - config[k] = v.(string) - } - - return NewConfig(config) + // read from config file if exists + b, err := ioutil.ReadFile(fn) + if err != nil { + log.Fatal("config file: ", err) + } + + // parse json config + var data map[string]interface{} + if err := json.Unmarshal(b, &data); err != nil { + log.Fatal("config file: ", err) + } + + _, ok := data["redirect_uri"] + if data["grant_type"] != "client_credentials" && !ok { + log.Fatal("config file: redirect_uri is missing in " + fn) + } + + // test required properties + for _, v := range requiredKeys { + _, ok := data[v] + if !ok { + log.Fatal("config file: " + v + " is missing in " + fn) + } + } + + // convert + config := make(map[string]string) + for k, v := range data { + config[k] = v.(string) + } + + return NewConfig(config) } // RoundTrip for the RoundTripper interface func (t *HeadersTransport) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set("User-Agent", t.uaHeader) + req.Header.Set("User-Agent", t.uaHeader) - if t.xTenantIdHeader != "" { - req.Header.Set("X-Upwork-API-TenantId", t.xTenantIdHeader) - } + if t.xTenantIdHeader != "" { + req.Header.Set("X-Upwork-API-TenantId", t.xTenantIdHeader) + } - return t.rt.RoundTrip(req) + return t.rt.RoundTrip(req) } // Configure X-Upwork-API-TenantId header for OwnHttpClient func (cfg *Config) SetOrgUidHeader(tenantId string) { - if (cfg.HasCustomHttpClient == true) { - panic("SetOrgUidHeader can not be used with the custom client. Add X-Upwork-API-TenantId header manually.") - } - cfg.TenantIdHeader = tenantId + if cfg.HasCustomHttpClient == true { + panic("SetOrgUidHeader can not be used with the custom client. Add X-Upwork-API-TenantId header manually.") + } + cfg.TenantIdHeader = tenantId } // Configure a context with the custom http client func (cfg *Config) SetCustomHttpClient(ctx context.Context, httpClient *http.Client) context.Context { - cfg.HasCustomHttpClient = true - return context.WithValue(ctx, oauth2.HTTPClient, httpClient) + cfg.HasCustomHttpClient = true + return context.WithValue(ctx, oauth2.HTTPClient, httpClient) } // Configure a context with the own http client over RoundTripper func (cfg *Config) SetOwnHttpClient(ctx context.Context) context.Context { - cfg.HasCustomHttpClient = false + cfg.HasCustomHttpClient = false - // Prepare wrapper to fix User-Agent header - var transport http.RoundTripper = &HeadersTransport{http.DefaultTransport, UPWORK_LIBRARY_USER_AGENT, cfg.TenantIdHeader} + // Prepare wrapper to fix User-Agent header + var transport http.RoundTripper = &HeadersTransport{http.DefaultTransport, UPWORK_LIBRARY_USER_AGENT, cfg.TenantIdHeader} - return context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: transport}) + return context.WithValue(ctx, oauth2.HTTPClient, &http.Client{Transport: transport}) } // Test print of found/assigned key func (cfg *Config) Print() { - fmt.Println("assigned client id (key):", cfg.ClientId) + fmt.Println("assigned client id (key):", cfg.ClientId) } diff --git a/example/example.go b/example/example.go index 0bfd442..71b7eb7 100644 --- a/example/example.go +++ b/example/example.go @@ -15,126 +15,135 @@ package main import ( - "fmt" - "bufio" - "os" - "context" // uncomment if you need to setup a custom http client - _ "net/http" // uncomment if you need to setup a custom http client - - _ "golang.org/x/oauth2" // uncomment if you need to work with oauth2.Token or other object, e.g. to store or re-cache token pair - - "github.com/upwork/golang-upwork-oauth2/api" - "github.com/upwork/golang-upwork-oauth2/api/routers/auth" - _ "github.com/upwork/golang-upwork-oauth2/api/routers/messages" // uncomment to test messages example - _ "github.com/upwork/golang-upwork-oauth2/api/routers/graphql" // uncomment to test graphql example + "bufio" + "context" // uncomment if you need to setup a custom http client + "fmt" + _ "net/http" // uncomment if you need to setup a custom http client + "os" + + _ "golang.org/x/oauth2" // uncomment if you need to work with oauth2.Token or other object, e.g. to store or re-cache token pair + + "github.com/upwork/golang-upwork-oauth2/api" + "github.com/upwork/golang-upwork-oauth2/api/routers/auth" + _ "github.com/upwork/golang-upwork-oauth2/api/routers/graphql" // uncomment to test graphql example + _ "github.com/upwork/golang-upwork-oauth2/api/routers/messages" // uncomment to test messages example ) const cfgFile = "config.json" // update the path to your config file, or provide properties directly in your code func main() { - // init context - ctx := context.Background() - -/* it is possible to set up properties from code - settings := map[string]string{ - "client_id": "clientid", - "client_secret": "clientsecret", - } - config := api.NewConfig(settings) - - //or read them from a specific configuration file - config := api.ReadConfig(cfgFile) - config.Print() -*/ - -/* it is possible to setup a custom http client if needed - httpClient := &http.Client{Timeout: 2} - config := api.ReadConfig(cfgFile) - ctx = config.SetCustomHttpClient(ctx, httpClient) - client := api.Setup(config) -*/ - ctx = context.TODO() // define NoContext if you do not use a custom client, otherwise use earlier defined context - client := api.Setup(api.ReadConfig(cfgFile)) - // You can configure the package send the requests as application/json, by default PostForm is used. - // This will be automatically set to true for GraphQL request - // client.SetPostAsJson(true) - - // GraphQL requests require X-Upwork-API-TenantId header, which can be setup using the following method - // client.SetOrgUidHeader(ctx, "1234567890") // Organization UID (optional) - -/* - // WARNING: oauth2 library will refresh the access token for you - // Setup notify function for refresh-token-workflow - // type Token, see https://godoc.org/golang.org/x/oauth2#Token - f := func(t *oauth2.Token) error { - // re-cache refreshed token - _, err := fmt.Printf("The token has been refreshed, here is a new one: %#v\n", t) - return err - } - client.SetRefreshTokenNotifyFunc(f) -*/ - // we need an access/refresh token pair in case we haven't received it yet - if !client.HasAccessToken(ctx) { - // required to authorize the application. Once you have an access/refresh token pair associated with - // the user, no need to redirect to the authorization screen. - aurl := client.GetAuthorizationUrl("random-state") - - // read code - reader := bufio.NewReader(os.Stdin) - fmt.Println("Visit the authorization url and provide oauth_verifier for further authorization") - fmt.Println(aurl) - authzCode, _ := reader.ReadString('\n') - - // WARNING: be sure to validate FormValue("state") before getting access token - - // get access token - token := client.GetToken(ctx, authzCode) - fmt.Println(token) // type Token, see https://godoc.org/golang.org/x/oauth2#Token - } - - // http.Response and specified type will be returned, you can use any - // use client.SetApiResponseType to specify the response type: use api.ByteResponse - // or api.ErrorResponse, see usage example below - // by default api.ByteResponse is used, i.e. []byte is returned as second value - _, jsonDataFromHttp1 := auth.New(&client).GetUserInfo() - - // here you can Unmarshal received json string, or do any other action(s) if you used ByteResponse - fmt.Println(string(jsonDataFromHttp1.([]byte))) // []byte - - // if you used ErrorResponse, like - // client.SetApiResponseType(api.ErrorResponse) - // httpResponse, err := auth.New(&client).GetUserInfo() - // if err == nil { - // ... do smth with http.Response - // } - - // run a post request using parameters as an example - // params := make(map[string]string) - // params["story"] = `{"message": "test message", "userId": "~017xxxxx"}` - // _, jsonDataFromHttp2 := messages.New(&client).SendMessageToRoom("company_id", "room_id", params) - // fmt.Println(string(jsonDataFromHttp2.([]byte))) - - // getting reports example - // params := make(map[string]string) - // params["tq"] = "select memo where worked_on >= '05-08-2015'" - // params["tqx"] = "out:json" - // _, jsonDataFromHttp3 := timereports.New(&client).GetByFreelancerFull(params) - // fmt.Println(string(jsonDataFromHttp3.([]byte))) - - // sending GraphQL request - // jsonData := map[string]string{ - // "query": ` - // { - // user { - // id - // nid - // } - // organization { - // id - // } - // } - // `, - // } - // _, jsonDataFromHttp4 := graphql.New(&client).Execute(jsonData) - // fmt.Println(string(jsonDataFromHttp4.([]byte))) + // init context + ctx := context.Background() + + /* it is possible to set up properties from code + settings := map[string]string{ + "client_id": "clientid", + "client_secret": "clientsecret", + } + config := api.NewConfig(settings) + + //or read them from a specific configuration file + config := api.ReadConfig(cfgFile) + config.Print() + */ + + /* it is possible to setup a custom http client if needed + httpClient := &http.Client{Timeout: 2} + config := api.ReadConfig(cfgFile) + ctx = config.SetCustomHttpClient(ctx, httpClient) + client := api.Setup(config) + */ + ctx = context.TODO() // define NoContext if you do not use a custom client, otherwise use earlier defined context + client := api.Setup(api.ReadConfig(cfgFile)) + // You can configure the package send the requests as application/json, by default PostForm is used. + // This will be automatically set to true for GraphQL request + // client.SetPostAsJson(true) + + // GraphQL requests require X-Upwork-API-TenantId header, which can be setup using the following method + // client.SetOrgUidHeader(ctx, "1234567890") // Organization UID (optional) + + /* + // -- Code Authorization Grant -- + // WARNING: oauth2 library will refresh the access token for you + // Setup notify function for refresh-token-workflow + // type Token, see https://godoc.org/golang.org/x/oauth2#Token + f := func(t *oauth2.Token) error { + // re-cache refreshed token + _, err := fmt.Printf("The token has been refreshed, here is a new one: %#v\n", t) + return err + } + client.SetRefreshTokenNotifyFunc(f) + */ + // we need an access/refresh token pair in case we haven't received it yet + if !client.HasAccessToken(ctx) { + // -- Code Authorization Grant -- + // required to authorize the application. Once you have an access/refresh token pair associated with + // the user, no need to redirect to the authorization screen. + + aurl := client.GetAuthorizationUrl("random-state") + + // read code + reader := bufio.NewReader(os.Stdin) + fmt.Println("Visit the authorization url and provide oauth_verifier for further authorization") + fmt.Println(aurl) + authzCode, _ := reader.ReadString('\n') + + // WARNING: be sure to validate FormValue("state") before getting access token + + // get access token + token := client.GetToken(ctx, authzCode) + // END -- Code Authorization Grant -- + + // -- Client Credentials Grant -- + // get access token, refreshed automatically + // token := client.GetToken(ctx, "") + + fmt.Println(token) // type Token, see https://godoc.org/golang.org/x/oauth2#Token + } + + // http.Response and specified type will be returned, you can use any + // use client.SetApiResponseType to specify the response type: use api.ByteResponse + // or api.ErrorResponse, see usage example below + // by default api.ByteResponse is used, i.e. []byte is returned as second value + _, jsonDataFromHttp1 := auth.New(&client).GetUserInfo() + + // here you can Unmarshal received json string, or do any other action(s) if you used ByteResponse + fmt.Println(string(jsonDataFromHttp1.([]byte))) // []byte + + // if you used ErrorResponse, like + // client.SetApiResponseType(api.ErrorResponse) + // httpResponse, err := auth.New(&client).GetUserInfo() + // if err == nil { + // ... do smth with http.Response + // } + + // run a post request using parameters as an example + // params := make(map[string]string) + // params["story"] = `{"message": "test message", "userId": "~017xxxxx"}` + // _, jsonDataFromHttp2 := messages.New(&client).SendMessageToRoom("company_id", "room_id", params) + // fmt.Println(string(jsonDataFromHttp2.([]byte))) + + // getting reports example + // params := make(map[string]string) + // params["tq"] = "select memo where worked_on >= '05-08-2015'" + // params["tqx"] = "out:json" + // _, jsonDataFromHttp3 := timereports.New(&client).GetByFreelancerFull(params) + // fmt.Println(string(jsonDataFromHttp3.([]byte))) + + // sending GraphQL request + // jsonData := map[string]string{ + // "query": ` + // { + // user { + // id + // nid + // } + // organization { + // id + // } + // } + // `, + // } + // _, jsonDataFromHttp4 := graphql.New(&client).Execute(jsonData) + // fmt.Println(string(jsonDataFromHttp4.([]byte))) } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..741e46e --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/upwork/golang-upwork-oauth2 + +go 1.19 + +require ( + github.com/stretchr/testify v1.8.4 + golang.org/x/oauth2 v0.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.11.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1405b4c --- /dev/null +++ b/go.sum @@ -0,0 +1,33 @@ +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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= +golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 0f366824f7dd52cacc0eacfbf7d12d101e3c5fb3 Mon Sep 17 00:00:00 2001 From: Maksym Novozhylov Date: Tue, 20 Jun 2023 08:58:51 +0200 Subject: [PATCH 2/5] fix(tests): downgrade testify to 1.5.0 --- go.mod | 4 ++-- go.sum | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 741e46e..4fd9bcd 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/upwork/golang-upwork-oauth2 go 1.19 require ( - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.5.0 golang.org/x/oauth2 v0.9.0 ) @@ -14,5 +14,5 @@ require ( golang.org/x/net v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 1405b4c..011a017 100644 --- a/go.sum +++ b/go.sum @@ -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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -8,8 +9,9 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw= +github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= @@ -29,5 +31,5 @@ google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscL google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 2020b5f934c33ead94e53089a4a76ab176bdf2e8 Mon Sep 17 00:00:00 2001 From: Maksym Novozhylov Date: Tue, 20 Jun 2023 09:05:59 +0200 Subject: [PATCH 3/5] fix: base host --- api/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/client.go b/api/client.go index 9c90352..d2d15d7 100644 --- a/api/client.go +++ b/api/client.go @@ -32,7 +32,7 @@ import ( // Define end points const ( // oauth2 and api flow - BaseHost = "https://stage.upwork.com/" + BaseHost = "https://www.upwork.com/" DefaultEpoint = "api" GqlEndpoint = "https://api.upwork.com/graphql" AuthorizationEP = BaseHost + "ab/account-security/oauth2/authorize" From aead62eec6fd4409837fa3cc51c0ac81c76383cf Mon Sep 17 00:00:00 2001 From: Maksym Novozhylov Date: Tue, 20 Jun 2023 09:07:09 +0200 Subject: [PATCH 4/5] build: update actions matrix --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71f8c31..582fac0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - go: [ '1.14', '1.15', '1.19' ] + go: [ '1.19', '1.20' ] steps: From 8c48eebde4d536835b90bf2bab722df1cc41a6d2 Mon Sep 17 00:00:00 2001 From: Maksym Novozhylov Date: Tue, 20 Jun 2023 09:08:29 +0200 Subject: [PATCH 5/5] revert(tests): testify back to 1.8.4 --- go.mod | 4 ++-- go.sum | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 4fd9bcd..741e46e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/upwork/golang-upwork-oauth2 go 1.19 require ( - github.com/stretchr/testify v1.5.0 + github.com/stretchr/testify v1.8.4 golang.org/x/oauth2 v0.9.0 ) @@ -14,5 +14,5 @@ require ( golang.org/x/net v0.11.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/yaml.v2 v2.2.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 011a017..1405b4c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -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/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -9,9 +8,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.0 h1:DMOzIV76tmoDNE9pX6RSN0aDtCYeCg5VueieJaAo1uw= -github.com/stretchr/testify v1.5.0/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= @@ -31,5 +29,5 @@ google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscL google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=