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

added OAuth2 options in http client #615

Merged
merged 7 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
42 changes: 42 additions & 0 deletions experimental/authclient/authclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package authclient

import (
"errors"
"net/http"

"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
)

// New auth client which is basically a http client but specific functionalities implemented depends on AuthMethod
func New(httpOptions httpclient.Options, options AuthOptions) (client *http.Client, err error) {
defaultClient, err := httpclient.New(httpOptions)
if err != nil {
return nil, err
}
switch options.AuthMethod {
case AuthMethodOAuth2:
Comment on lines +16 to +17
Copy link
Contributor

@andresmgot andresmgot Feb 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I think doesn't make sense from the code perspective. If you name this package oauth2client, you don't need the AuthMethod. The client, rather than setting this Method based on the user input, can call the normal httpClient.New or this oauth2Client.New.

If in the future, if there is a third auth type, then you can create the abstraction of an authclient that can either instantiate an httpClient, an oauth2Client or something else but for the case of only 2 types I don't think it's necessary this extra complexity here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still believe we need authentication method.

In this PR, I am adding only support for OAuth2. But in near future, we need more ways to authenticate as seen here.

Ideally I don't like to create New for every single auth types such as oauthclient.New, digestauthclient.New.. If we want to make some changes such as adding a custom header I don't want to add in every places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not a fan of merging code that is meant for the future but since this is still experimental it's not a blocker 👍

if options.OAuth2Options == nil {
return nil, errors.New("invalid options for OAuth2 client")
}
return getOAuth2Client(defaultClient, *options.OAuth2Options)
default:
return defaultClient, nil
}
}

// AuthOptions Auth client options. Based on the AuthenticationMethod, further properties will be validated
type AuthOptions struct {
// AuthMethod ...
AuthMethod AuthMethod
// OAuth2Options ...
OAuth2Options *OAuth2Options
}

// AuthMethod defines the type of authentication method that needs to be use.
type AuthMethod string

const (
// AuthMethodOAuth2 is oauth2 type authentication.
// Currently support client credentials and JWT type OAuth2 workflows.
AuthMethodOAuth2 AuthMethod = "oauth2"
)
157 changes: 157 additions & 0 deletions experimental/authclient/authclient_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package authclient_test

import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/experimental/authclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNew(t *testing.T) {
t.Run("oauth2", func(t *testing.T) {
server := getOAuthServer(t)
defer server.Close()
t.Run("client credentials", func(t *testing.T) {
t.Run("valid client credentials", func(t *testing.T) {
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
OAuth2Type: authclient.OAuth2TypeClientCredentials,
TokenURL: server.URL + "/token",
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
require.Nil(t, err)
require.NotNil(t, res)
if res != nil && res.Body != nil {
defer res.Body.Close()
}
bodyBytes, err := io.ReadAll(res.Body)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, `"hello world"`, string(bodyBytes))
})
t.Run("valid client credentials with basic auth settings", func(t *testing.T) {
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
BasicAuth: &httpclient.BasicAuthOptions{User: "userFoo", Password: "pwdBar"},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
OAuth2Type: authclient.OAuth2TypeClientCredentials,
TokenURL: server.URL + "/token",
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
require.Nil(t, err)
require.NotNil(t, res)
if res != nil && res.Body != nil {
defer res.Body.Close()
}
bodyBytes, err := io.ReadAll(res.Body)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, `"hello world"`, string(bodyBytes))
})
})
t.Run("jwt", func(t *testing.T) {
t.Run("invalid private key", func(t *testing.T) {
privateKey := generateKey(t, true)
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
OAuth2Type: authclient.OAuth2TypeJWT,
TokenURL: server.URL + "/token",
PrivateKey: privateKey,
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
if res != nil && res.Body != nil {
defer res.Body.Close()
}
require.NotNil(t, err)
assert.True(t, strings.Contains(err.Error(), "private key should be a PEM or plain PKCS1 or PKCS8; parse error: asn1: structure error"))
require.Nil(t, res)
})
t.Run("valid private key", func(t *testing.T) {
privateKey := generateKey(t, false)
hc, err := authclient.New(httpclient.Options{
Headers: map[string]string{"h1": "v1"},
}, authclient.AuthOptions{
AuthMethod: authclient.AuthMethodOAuth2,
OAuth2Options: &authclient.OAuth2Options{
OAuth2Type: authclient.OAuth2TypeJWT,
TokenURL: server.URL + "/token",
PrivateKey: privateKey,
},
})
require.Nil(t, err)
require.NotNil(t, hc)
res, err := hc.Get(server.URL + "/foo")
require.Nil(t, err)
require.NotNil(t, res)
if res != nil && res.Body != nil {
defer res.Body.Close()
}
bodyBytes, err := io.ReadAll(res.Body)
require.Nil(t, err)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Equal(t, `"hello world"`, string(bodyBytes))
})
})
})
}

func getOAuthServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
oAuth2TokenValue := "foo"
t.Run("ensure custom headers propagated correctly", func(t *testing.T) {
require.Equal(t, "v1", r.Header.Get("h1"))
})
if r.URL.String() == "/token" {
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, fmt.Sprintf(`{"access_token": "%s", "refresh_token": "bar"}`, oAuth2TokenValue))
return
}
t.Run("ensure oauth token correctly sets to the authorization header", func(t *testing.T) {
require.Equal(t, fmt.Sprintf("Bearer %s", oAuth2TokenValue), r.Header.Get("Authorization"))
})
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `"hello world"`)
}))
}

func generateKey(t *testing.T, incorrectKey bool) (privateKey []byte) {
t.Helper()
if incorrectKey {
return []byte("invalid private key")
}
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
panic(err)
}
privateKey = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
return privateKey
}
96 changes: 96 additions & 0 deletions experimental/authclient/oauth2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package authclient

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"

"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"golang.org/x/oauth2/jwt"
)

// OAuth2Type defines type of oauth2 grant type
type OAuth2Type string

const (
OAuth2TypeClientCredentials OAuth2Type = "client_credentials"
OAuth2TypeJWT OAuth2Type = "jwt"
)

// OAuth2Options defines options for OAuth2 Client
type OAuth2Options struct {
OAuth2Type OAuth2Type
TokenURL string
Scopes []string
ClientID string
ClientSecret string
EndpointParams map[string]string
Subject string
Email string
PrivateKey []byte
PrivateKeyID string
}

func getOAuth2Client(client *http.Client, oAuth2Options OAuth2Options) (o *http.Client, err error) {
switch oAuth2Options.OAuth2Type {
case OAuth2TypeClientCredentials:
return getOAuth2ClientCredentialsClient(client, oAuth2Options)
case OAuth2TypeJWT:
return getOAuth2JWTClient(client, oAuth2Options)
}
return client, fmt.Errorf("invalid/empty oauth2 type (%s)", oAuth2Options.OAuth2Type)
}

func getOAuth2ClientCredentialsClient(client *http.Client, options OAuth2Options) (*http.Client, error) {
config := clientcredentials.Config{
TokenURL: options.TokenURL,
Scopes: sanitizeOAuth2Scopes(options),
EndpointParams: sanitizeOAuth2EndpointParams(options),
ClientID: options.ClientID,
ClientSecret: options.ClientSecret,
}
if client == nil {
return config.Client(context.Background()), nil
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)
return config.Client(ctx), nil
}

func getOAuth2JWTClient(client *http.Client, options OAuth2Options) (*http.Client, error) {
config := jwt.Config{
TokenURL: options.TokenURL,
Scopes: sanitizeOAuth2Scopes(options),
PrivateKey: options.PrivateKey,
PrivateKeyID: options.PrivateKeyID,
Email: options.Email,
Subject: options.Subject,
}
if client == nil {
return config.Client(context.Background()), nil
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, client)
return config.Client(ctx), nil
}

func sanitizeOAuth2Scopes(options OAuth2Options) []string {
scopes := []string{}
for _, scope := range options.Scopes {
if scope != "" {
scopes = append(scopes, strings.TrimSpace(scope))
}
}
return scopes
}

func sanitizeOAuth2EndpointParams(options OAuth2Options) url.Values {
endpointParams := url.Values{}
for k, v := range options.EndpointParams {
if k != "" && v != "" {
endpointParams.Set(strings.TrimSpace(k), strings.TrimSpace(v))
}
}
return endpointParams
}
10 changes: 7 additions & 3 deletions experimental/testdata/folder.golden.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,31 @@

Frame[0] {
"type": "directory-listing",
"typeVersion": [
0,
0
],
"pathSeparator": "/"
}
Name:
Dimensions: 2 Fields by 14 Rows
Dimensions: 2 Fields by 15 Rows
+---------------------------------+------------------+
| Name: name | Name: media-type |
| Labels: | Labels: |
| Type: []string | Type: []string |
+---------------------------------+------------------+
| actions | directory |
| authclient | directory |
| e2e | directory |
| fileinfo.go | |
| fileinfo_test.go | |
| frame_sorter.go | |
| frame_sorter_test.go | |
| golden_response_checker.go | |
| golden_response_checker_test.go | |
| http_logger | directory |
| ... | ... |
+---------------------------------+------------------+


====== TEST DATA RESPONSE (arrow base64) ======
FRAME=QVJST1cxAAD/////uAEAABAAAAAAAAoADgAMAAsABAAKAAAAFAAAAAAAAAEEAAoADAAAAAgABAAKAAAACAAAAKQAAAADAAAATAAAACgAAAAEAAAA1P7//wgAAAAMAAAAAAAAAAAAAAAFAAAAcmVmSWQAAAD0/v//CAAAAAwAAAAAAAAAAAAAAAQAAABuYW1lAAAAABT///8IAAAAPAAAADAAAAB7InR5cGUiOiJkaXJlY3RvcnktbGlzdGluZyIsInBhdGhTZXBhcmF0b3IiOiIvIn0AAAAABAAAAG1ldGEAAAAAAgAAAHgAAAAEAAAAov///xQAAAA8AAAAPAAAAAAAAAU4AAAAAQAAAAQAAACQ////CAAAABAAAAAGAAAAc3RyaW5nAAAGAAAAdHN0eXBlAAAAAAAAiP///woAAABtZWRpYS10eXBlAAAAABIAGAAUAAAAEwAMAAAACAAEABIAAAAUAAAARAAAAEgAAAAAAAAFRAAAAAEAAAAMAAAACAAMAAgABAAIAAAACAAAABAAAAAGAAAAc3RyaW5nAAAGAAAAdHN0eXBlAAAAAAAABAAEAAQAAAAEAAAAbmFtZQAAAAAAAAAA/////9gAAAAUAAAAAAAAAAwAFgAUABMADAAEAAwAAAB4AQAAAAAAABQAAAAAAAADBAAKABgADAAIAAQACgAAABQAAAB4AAAADgAAAAAAAAAAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAQAAAAAAAAACxAAAAAAAAAPgAAAAAAAAAAAAAAAAAAAD4AAAAAAAAADwAAAAAAAAAOAEAAAAAAAA/AAAAAAAAAAAAAAACAAAADgAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAABwAAAAoAAAAVAAAAJQAAADQAAABIAAAAYgAAAIEAAACMAAAAkgAAAJYAAACkAAAAqQAAALEAAAAAAAAAYWN0aW9uc2UyZWZpbGVpbmZvLmdvZmlsZWluZm9fdGVzdC5nb2ZyYW1lX3NvcnRlci5nb2ZyYW1lX3NvcnRlcl90ZXN0LmdvZ29sZGVuX3Jlc3BvbnNlX2NoZWNrZXIuZ29nb2xkZW5fcmVzcG9uc2VfY2hlY2tlcl90ZXN0LmdvaHR0cF9sb2dnZXJtYWNyb3Ntb2NrcmVzdF9jbGllbnQuZ29zZGF0YXRlc3RkYXRhAAAAAAAAAAAAAAAJAAAAEgAAABIAAAASAAAAEgAAABIAAAASAAAAEgAAABsAAAAkAAAALQAAAC0AAAA2AAAAPwAAAAAAAABkaXJlY3RvcnlkaXJlY3RvcnlkaXJlY3RvcnlkaXJlY3RvcnlkaXJlY3RvcnlkaXJlY3RvcnlkaXJlY3RvcnkAEAAAAAwAFAASAAwACAAEAAwAAAAQAAAALAAAADgAAAAAAAQAAQAAAMgBAAAAAAAA4AAAAAAAAAB4AQAAAAAAAAAAAAAAAAAAAAAKAAwAAAAIAAQACgAAAAgAAACkAAAAAwAAAEwAAAAoAAAABAAAANT+//8IAAAADAAAAAAAAAAAAAAABQAAAHJlZklkAAAA9P7//wgAAAAMAAAAAAAAAAAAAAAEAAAAbmFtZQAAAAAU////CAAAADwAAAAwAAAAeyJ0eXBlIjoiZGlyZWN0b3J5LWxpc3RpbmciLCJwYXRoU2VwYXJhdG9yIjoiLyJ9AAAAAAQAAABtZXRhAAAAAAIAAAB4AAAABAAAAKL///8UAAAAPAAAADwAAAAAAAAFOAAAAAEAAAAEAAAAkP///wgAAAAQAAAABgAAAHN0cmluZwAABgAAAHRzdHlwZQAAAAAAAIj///8KAAAAbWVkaWEtdHlwZQAAAAASABgAFAAAABMADAAAAAgABAASAAAAFAAAAEQAAABIAAAAAAAABUQAAAABAAAADAAAAAgADAAIAAQACAAAAAgAAAAQAAAABgAAAHN0cmluZwAABgAAAHRzdHlwZQAAAAAAAAQABAAEAAAABAAAAG5hbWUAAAAA4AEAAEFSUk9XMQ==
FRAME=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
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
github.com/google/uuid v1.3.0
github.com/unknwon/bra v0.0.0-20200517080246-1e3013ecaff8
github.com/urfave/cli v1.22.1
golang.org/x/oauth2 v0.5.0
golang.org/x/text v0.7.0
)

Expand Down Expand Up @@ -71,6 +72,7 @@ require (
github.com/unknwon/log v0.0.0-20150304194804-e617c87089d3 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210630183607-d20f26d13c79 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
Expand All @@ -284,6 +285,8 @@ golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -311,6 +314,7 @@ golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down Expand Up @@ -343,6 +347,8 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
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/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
Expand Down