diff --git a/cspell.config.json b/cspell.config.json index d9bbad9f..8c39667b 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -15,7 +15,8 @@ "words": [ "adxauth", "adxcredentials", - "adxusercontext", + "azhttpclient", + "azusercontext", "clientsecret", "Codeowners", "datasource", diff --git a/go.mod b/go.mod index ed6677f4..377e2585 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 github.com/AzureAD/microsoft-authentication-library-for-go v0.7.0 github.com/google/go-cmp v0.5.9 - github.com/grafana/grafana-azure-sdk-go v1.5.2 + github.com/grafana/grafana-azure-sdk-go v1.6.0 github.com/grafana/grafana-plugin-sdk-go v0.147.0 github.com/json-iterator/go v1.1.12 github.com/stretchr/testify v1.8.1 diff --git a/go.sum b/go.sum index ec727ece..1c91ecef 100644 --- a/go.sum +++ b/go.sum @@ -102,7 +102,6 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/getkin/kin-openapi v0.91.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo= github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -176,7 +175,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -200,10 +198,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grafana/grafana-azure-sdk-go v1.5.2 h1:rjN5WIJ1GS/K5SKT1WPRKynsdvHFqU/SvAigCp0zSp4= -github.com/grafana/grafana-azure-sdk-go v1.5.2/go.mod h1:OJJuBJ3MOoaq2mqD6xlPsArpL2R5j80TrDqPYr35Zak= -github.com/grafana/grafana-plugin-sdk-go v0.129.0/go.mod h1:4edtosZepfQF9jkQwRywJsNSJzXTHmzbmcVcAl8MEQc= -github.com/grafana/grafana-plugin-sdk-go v0.129.0/go.mod h1:4edtosZepfQF9jkQwRywJsNSJzXTHmzbmcVcAl8MEQc= +github.com/grafana/grafana-azure-sdk-go v1.6.0 h1:lxvH/mVY7gKBtJKhZ4B/6tIZFY7Jth97HxBA38olaxs= +github.com/grafana/grafana-azure-sdk-go v1.6.0/go.mod h1:X4PdEQIYgHfn0KTa2ZTKvufhNz6jbCEKUQPZIlcyOGw= github.com/grafana/grafana-plugin-sdk-go v0.147.0 h1:VavvJOa/Ubs+wzalzWIl+FQmdaD4vEK8KVYU0a8rf+E= github.com/grafana/grafana-plugin-sdk-go v0.147.0/go.mod h1:NMgO3t2gR5wyLx8bWZ9CTmpDk5Txp4wYFccFLHdYn3Q= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= @@ -253,7 +249,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/magefile/mage v1.12.1/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo= github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= diff --git a/pkg/azuredx/adxauth/adxusercontext/context.go b/pkg/azuredx/adxauth/adxusercontext/context.go deleted file mode 100644 index 0ed712d9..00000000 --- a/pkg/azuredx/adxauth/adxusercontext/context.go +++ /dev/null @@ -1,25 +0,0 @@ -package adxusercontext - -import ( - "context" - - "github.com/grafana/grafana-plugin-sdk-go/backend" -) - -type userCtxKey struct { -} - -type CurrentUserContext struct { - User *backend.User - IdToken string - AccessToken string -} - -func WithCurrentUser(ctx context.Context, currentUser CurrentUserContext) context.Context { - return context.WithValue(ctx, userCtxKey{}, currentUser) -} - -func GetCurrentUser(ctx context.Context) (CurrentUserContext, bool) { - resourceReq, ok := ctx.Value(userCtxKey{}).(CurrentUserContext) - return resourceReq, ok -} diff --git a/pkg/azuredx/adxauth/adxusercontext/from_req.go b/pkg/azuredx/adxauth/adxusercontext/from_req.go deleted file mode 100644 index 764e1778..00000000 --- a/pkg/azuredx/adxauth/adxusercontext/from_req.go +++ /dev/null @@ -1,110 +0,0 @@ -package adxusercontext - -import ( - "context" - "strings" - - "github.com/grafana/grafana-plugin-sdk-go/backend" -) - -func WithUserFromQueryReq(ctx context.Context, req *backend.QueryDataRequest) context.Context { - if req == nil { - return ctx - } - - idToken := getQueryReqHeader(req, "X-ID-Token") - accessToken := extractBearerToken(getQueryReqHeader(req, "Authorization")) - - currentUser := CurrentUserContext{ - User: req.PluginContext.User, - IdToken: idToken, - AccessToken: accessToken, - } - - return WithCurrentUser(ctx, currentUser) -} - -func WithUserFromResourceReq(ctx context.Context, req *backend.CallResourceRequest) context.Context { - if req == nil { - return ctx - } - - idToken := getResourceReqHeader(req, "X-ID-Token") - accessToken := extractBearerToken(getResourceReqHeader(req, "Authorization")) - - currentUser := CurrentUserContext{ - User: req.PluginContext.User, - IdToken: idToken, - AccessToken: accessToken, - } - - return WithCurrentUser(ctx, currentUser) -} - -func WithUserFromHealthCheckReq(ctx context.Context, req *backend.CheckHealthRequest) context.Context { - if req == nil { - return ctx - } - - idToken := getCheckHealthReqHeader(req, "X-ID-Token") - accessToken := extractBearerToken(getCheckHealthReqHeader(req, "Authorization")) - - currentUser := CurrentUserContext{ - User: req.PluginContext.User, - IdToken: idToken, - AccessToken: accessToken, - } - - return WithCurrentUser(ctx, currentUser) -} - -func getQueryReqHeader(req *backend.QueryDataRequest, headerName string) string { - headerNameCI := strings.ToLower(headerName) - - for name, value := range req.Headers { - if strings.ToLower(name) == headerNameCI { - return value - } - } - - return "" -} - -func getResourceReqHeader(req *backend.CallResourceRequest, headerName string) string { - headerNameCI := strings.ToLower(headerName) - - for name, values := range req.Headers { - if strings.ToLower(name) == headerNameCI { - if len(values) > 0 { - return values[0] - } else { - return "" - } - } - } - - return "" -} - -func getCheckHealthReqHeader(req *backend.CheckHealthRequest, headerName string) string { - headerNameCI := strings.ToLower(headerName) - - for name, value := range req.Headers { - if strings.ToLower(name) == headerNameCI { - return value - } - } - - return "" -} - -func extractBearerToken(authorizationHeader string) string { - const bearerPrefix = "Bearer " - - var accessToken string - if strings.HasPrefix(authorizationHeader, bearerPrefix) { - accessToken = strings.TrimPrefix(authorizationHeader, bearerPrefix) - } - - return accessToken -} diff --git a/pkg/azuredx/adxauth/auth.go b/pkg/azuredx/adxauth/auth.go deleted file mode 100644 index 7a7a7f93..00000000 --- a/pkg/azuredx/adxauth/auth.go +++ /dev/null @@ -1,111 +0,0 @@ -package adxauth - -import ( - "context" - "fmt" - "net/http" - "time" - - "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth/adxusercontext" - "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/models" - "github.com/grafana/grafana-azure-sdk-go/azcredentials" - "github.com/grafana/grafana-azure-sdk-go/azsettings" - "github.com/grafana/grafana-azure-sdk-go/aztokenprovider" - "github.com/grafana/grafana-plugin-sdk-go/backend" -) - -// ServiceCredentials provides authorization for cloud service usage. -type ServiceCredentials interface { - GetAccessToken(ctx context.Context) (string, error) -} - -type ServiceCredentialsImpl struct { - QueryTimeout time.Duration - - tokenProvider aztokenprovider.AzureTokenProvider - aadClient aadClient - scopes []string -} - -func NewServiceCredentials(settings *models.DatasourceSettings, azureSettings *azsettings.AzureSettings, - credentials azcredentials.AzureCredentials) (ServiceCredentials, error) { - var err error - - var tokenProvider aztokenprovider.AzureTokenProvider - var aadClient aadClient = nil - - switch c := credentials.(type) { - case *azcredentials.AzureClientSecretOboCredentials: - // Special support for OBO authentication as it isn't supported by the SDK - // Configure the service identity token provider with underlying client secret credentials - tokenProvider, err = aztokenprovider.NewAzureAccessTokenProvider(azureSettings, &c.ClientSecretCredentials) - if err != nil { - return nil, fmt.Errorf("invalid Azure configuration: %w", err) - } - aadClient, err = newAADClient(&c.ClientSecretCredentials, http.DefaultClient) - if err != nil { - return nil, fmt.Errorf("invalid Azure configuration: %w", err) - } - default: - tokenProvider, err = aztokenprovider.NewAzureAccessTokenProvider(azureSettings, c) - if err != nil { - return nil, fmt.Errorf("invalid Azure configuration: %w", err) - } - } - - scopes, err := getAzureScopes(azureSettings, credentials, settings.ClusterURL) - if err != nil { - return nil, fmt.Errorf("invalid Azure configuration: %w", err) - } - - return &ServiceCredentialsImpl{ - QueryTimeout: settings.QueryTimeout, - tokenProvider: tokenProvider, - aadClient: aadClient, - scopes: scopes, - }, nil -} - -// GetAccessToken returns access token for configured credentials -func (c *ServiceCredentialsImpl) GetAccessToken(ctx context.Context) (string, error) { - if ctx == nil { - err := fmt.Errorf("parameter 'ctx' cannot be nil") - return "", err - } - - if c.QueryTimeout != 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, c.QueryTimeout) - defer cancel() - } - - // Use AAD client if initialized (for OBO credentials) - if c.aadClient != nil { - return c.onBehalfOf(ctx) - } else { - // Service identity credentials - return c.tokenProvider.GetAccessToken(ctx, c.scopes) - } -} - -func (c *ServiceCredentialsImpl) onBehalfOf(ctx context.Context) (string, error) { - currentUser, ok := adxusercontext.GetCurrentUser(ctx) - if !ok { - err := fmt.Errorf("user context not configured") - return "", err - } - - if currentUser.IdToken == "" { - err := fmt.Errorf("user context doesn't have ID token") - return "", err - } - - result, err := c.aadClient.AcquireTokenOnBehalfOf(ctx, currentUser.IdToken, c.scopes) - if err != nil { - backend.Logger.Error(err.Error(), "user", currentUser.User.Login) - err = fmt.Errorf("unable to acquire access token for user '%s'", currentUser.User.Login) - return "", err - } - - return result.AccessToken, nil -} diff --git a/pkg/azuredx/adxauth/auth_test.go b/pkg/azuredx/adxauth/auth_test.go deleted file mode 100644 index 2668f62b..00000000 --- a/pkg/azuredx/adxauth/auth_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package adxauth - -import ( - "context" - "testing" - - "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" - "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth/adxusercontext" - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/stretchr/testify/assert" -) - -func TestOnBehalfOf(t *testing.T) { - golden := []struct { - CurrentUser *adxusercontext.CurrentUserContext - OnBehalfOfDisabled bool - ShouldRequestToken bool - ExpectedError string - }{ - // happy flow - 0: { - CurrentUser: &adxusercontext.CurrentUserContext{ - User: &backend.User{Login: "alice"}, - IdToken: "ID-TOKEN", - }, - ShouldRequestToken: true, - ExpectedError: "", - }, - - 1: { - CurrentUser: nil, - ShouldRequestToken: false, - ExpectedError: "user context not configured", - }, - - 2: { - CurrentUser: &adxusercontext.CurrentUserContext{ - User: &backend.User{Login: "alice"}, - }, - ShouldRequestToken: false, - ExpectedError: "user context doesn't have ID token", - }, - - 3: { - CurrentUser: &adxusercontext.CurrentUserContext{ - User: &backend.User{Login: "alice"}, - IdToken: "ID-TOKEN", - }, - OnBehalfOfDisabled: true, - ShouldRequestToken: false, - }, - } - - for index, g := range golden { - // setup & test - fakeAADClient := &FakeAADClient{} - fakeTokenProvider := &FakeTokenProvider{} - - c := &ServiceCredentialsImpl{ - tokenProvider: fakeTokenProvider, - } - if !g.OnBehalfOfDisabled { - c.aadClient = fakeAADClient - } - - ctx := context.Background() - if g.CurrentUser != nil { - ctx = adxusercontext.WithCurrentUser(ctx, *g.CurrentUser) - } - - auth, err := c.GetAccessToken(ctx) - - switch { - case err != nil: - switch { - case g.ExpectedError == "": - t.Errorf("%d: got error %q", index, err) - case err.Error() != g.ExpectedError: - t.Errorf("%d: got error %q, expected %q", index, err, g.ExpectedError) - } - - case g.ExpectedError != "": - t.Errorf("%d: got authorization %q, want error %q", index, auth, g.ExpectedError) - - case g.OnBehalfOfDisabled: - assert.Equal(t, fakeTokenProvider.TokenRequested, true) - assert.Equal(t, fakeAADClient.TokenRequested, false) - - case g.ShouldRequestToken != fakeAADClient.TokenRequested: - t.Errorf("%d: should request token = %t, requested = %t", index, g.ShouldRequestToken, fakeAADClient.TokenRequested) - - default: - assert.Equal(t, fakeTokenProvider.TokenRequested, false) - } - } -} - -type FakeAADClient struct { - TokenRequested bool -} - -func (c *FakeAADClient) AcquireTokenOnBehalfOf(_ context.Context, _ string, _ []string) (confidential.AuthResult, error) { - c.TokenRequested = true - return confidential.AuthResult{}, nil -} - -type FakeTokenProvider struct { - TokenRequested bool -} - -func (tp *FakeTokenProvider) GetAccessToken(_ context.Context, _ []string) (string, error) { - tp.TokenRequested = true - return "Bearer ok", nil -} diff --git a/pkg/azuredx/adxauth/obo_token_provider.go b/pkg/azuredx/adxauth/obo_token_provider.go new file mode 100644 index 00000000..e06f951b --- /dev/null +++ b/pkg/azuredx/adxauth/obo_token_provider.go @@ -0,0 +1,74 @@ +package adxauth + +import ( + "context" + "fmt" + "net/http" + + "github.com/grafana/grafana-azure-sdk-go/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-azure-sdk-go/aztokenprovider" + "github.com/grafana/grafana-azure-sdk-go/azusercontext" +) + +type onBehalfOfTokenProvider struct { + aadClient aadClient +} + +func NewOnBehalfOfAccessTokenProvider(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials) (aztokenprovider.AzureTokenProvider, error) { + var err error + + if settings == nil { + err = fmt.Errorf("parameter 'settings' cannot be nil") + return nil, err + } + if credentials == nil { + err = fmt.Errorf("parameter 'credentials' cannot be nil") + return nil, err + } + + switch c := credentials.(type) { + case *azcredentials.AzureClientSecretOboCredentials: + aadClient, err := newAADClient(&c.ClientSecretCredentials, http.DefaultClient) + if err != nil { + return nil, fmt.Errorf("invalid Azure configuration: %w", err) + } + return &onBehalfOfTokenProvider{ + aadClient: aadClient, + }, nil + default: + err = fmt.Errorf("credentials of type '%s' not supported by the on-behalf-of token provider", c.AzureAuthType()) + return nil, err + } +} + +func (provider *onBehalfOfTokenProvider) GetAccessToken(ctx context.Context, scopes []string) (string, error) { + if ctx == nil { + err := fmt.Errorf("parameter 'ctx' cannot be nil") + return "", err + } + + if scopes == nil { + err := fmt.Errorf("parameter 'scopes' cannot be nil") + return "", err + } + + currentUser, ok := azusercontext.GetCurrentUser(ctx) + if !ok { + err := fmt.Errorf("user context not configured") + return "", err + } + + if currentUser.IdToken == "" { + err := fmt.Errorf("user context doesn't have ID token") + return "", err + } + + result, err := provider.aadClient.AcquireTokenOnBehalfOf(ctx, currentUser.IdToken, scopes) + if err != nil { + err = fmt.Errorf("unable to acquire access token: %w", err) + return "", err + } + + return result.AccessToken, nil +} diff --git a/pkg/azuredx/client/client.go b/pkg/azuredx/client/client.go index b3d8e2d4..4b822e53 100644 --- a/pkg/azuredx/client/client.go +++ b/pkg/azuredx/client/client.go @@ -6,7 +6,9 @@ import ( "fmt" "net/http" - "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth" + "github.com/grafana/grafana-azure-sdk-go/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend" // 100% compatible drop-in replacement of "encoding/json" json "github.com/json-iterator/go" @@ -22,13 +24,16 @@ var _ AdxClient = new(Client) // validates interface conformance // Client is an http.Client used for API requests. type Client struct { - httpClient *http.Client - serviceCredentials adxauth.ServiceCredentials + httpClient *http.Client } // NewClient creates a Grafana Plugin SDK Go Http Client -func New(serviceCredentials adxauth.ServiceCredentials, client *http.Client) *Client { - return &Client{serviceCredentials: serviceCredentials, httpClient: client} +func New(instanceSettings *backend.DataSourceInstanceSettings, dsSettings *models.DatasourceSettings, azureSettings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials) (*Client, error) { + httpClient, err := newHttpClient(instanceSettings, dsSettings, azureSettings, credentials) + if err != nil { + return nil, err + } + return &Client{httpClient: httpClient}, nil } // TestRequest handles a data source test request in Grafana's Datasource configuration UI. @@ -42,19 +47,12 @@ func (c *Client) TestRequest(ctx context.Context, datasourceSettings *models.Dat return err } - // TODO: This is a workaround because Plugin SDK doesn't expose user context for CheckHealth - accessToken, err := c.serviceCredentials.GetAccessToken(ctx) - if err != nil { - return err - } - - req, err := http.NewRequest("POST", datasourceSettings.ClusterURL+"/v1/rest/query", bytes.NewReader(buf)) + req, err := http.NewRequestWithContext(ctx, "POST", datasourceSettings.ClusterURL+"/v1/rest/query", bytes.NewReader(buf)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) for key, value := range additionalHeaders { req.Header.Set(key, value) } @@ -80,19 +78,13 @@ func (c *Client) KustoRequest(ctx context.Context, url string, payload models.Re return nil, fmt.Errorf("no Azure request serial: %w", err) } - accessToken, err := c.serviceCredentials.GetAccessToken(ctx) - if err != nil { - return nil, err - } - - req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(buf)) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(buf)) if err != nil { return nil, fmt.Errorf("no Azure request instance: %w", err) } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+accessToken) req.Header.Set("x-ms-app", "Grafana-ADX") if payload.QuerySource == "" { payload.QuerySource = "unspecified" diff --git a/pkg/azuredx/client/client_test.go b/pkg/azuredx/client/client_test.go index ae2b6a5a..edfbcbc4 100644 --- a/pkg/azuredx/client/client_test.go +++ b/pkg/azuredx/client/client_test.go @@ -34,7 +34,7 @@ func TestClient(t *testing.T) { QuerySource: "schema", } - client := New(&fakeCredentialsProvider{}, server.Client()) + client := &Client{httpClient: server.Client()} table, err := client.KustoRequest(context.Background(), server.URL, payload, nil) require.NoError(t, err) require.NotNil(t, table) @@ -61,7 +61,7 @@ func TestClient(t *testing.T) { QuerySource: "schema", } - client := New(&fakeCredentialsProvider{}, server.Client()) + client := &Client{httpClient: server.Client()} table, err := client.KustoRequest(context.Background(), server.URL, payload, nil) require.Nil(t, table) require.NotNil(t, err) @@ -88,7 +88,7 @@ func TestClient(t *testing.T) { "x-ms-client-request-id": "KGC.schema;deadbeef", } - client := New(&fakeCredentialsProvider{}, server.Client()) + client := &Client{httpClient: server.Client()} table, err := client.KustoRequest(context.Background(), server.URL, payload, headers) require.Nil(t, table) require.NotNil(t, err) @@ -102,9 +102,3 @@ func loadTestFile(path string) ([]byte, error) { } return jsonBody, nil } - -type fakeCredentialsProvider struct{} - -func (c *fakeCredentialsProvider) GetAccessToken(_ context.Context) (string, error) { - return "FAKE_TOKEN", nil -} diff --git a/pkg/azuredx/client/httpclient.go b/pkg/azuredx/client/httpclient.go new file mode 100644 index 00000000..83f8df70 --- /dev/null +++ b/pkg/azuredx/client/httpclient.go @@ -0,0 +1,42 @@ +package client + +import ( + "fmt" + "net/http" + + "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth" + "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/models" + "github.com/grafana/grafana-azure-sdk-go/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/azhttpclient" + "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" +) + +func newHttpClient(instanceSettings *backend.DataSourceInstanceSettings, dsSettings *models.DatasourceSettings, azureSettings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials) (*http.Client, error) { + authOpts := azhttpclient.NewAuthOptions(azureSettings) + + // TODO: #555 configure on-behalf-of authentication if enabled in AzureSettings + authOpts.AddTokenProvider(azcredentials.AzureAuthClientSecretObo, adxauth.NewOnBehalfOfAccessTokenProvider) + + scopes, err := getAdxScopes(azureSettings, credentials, dsSettings.ClusterURL) + if err != nil { + return nil, err + } + authOpts.Scopes(scopes) + + clientOpts, err := instanceSettings.HTTPClientOptions() + if err != nil { + return nil, fmt.Errorf("error creating http client: %w", err) + } + clientOpts.Timeouts.Timeout = dsSettings.QueryTimeout + + azhttpclient.AddAzureAuthentication(&clientOpts, authOpts, credentials) + + httpClient, err := httpclient.NewProvider().New(clientOpts) + if err != nil { + return nil, fmt.Errorf("error creating http client: %w", err) + } + + return httpClient, nil +} diff --git a/pkg/azuredx/adxauth/scopes.go b/pkg/azuredx/client/scopes.go similarity index 86% rename from pkg/azuredx/adxauth/scopes.go rename to pkg/azuredx/client/scopes.go index 8aade468..3b6af40c 100644 --- a/pkg/azuredx/adxauth/scopes.go +++ b/pkg/azuredx/client/scopes.go @@ -1,4 +1,4 @@ -package adxauth +package client import ( "fmt" @@ -16,7 +16,7 @@ var ( } ) -func getAzureScopes(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials, clusterUrl string) ([]string, error) { +func getAdxScopes(settings *azsettings.AzureSettings, credentials azcredentials.AzureCredentials, clusterUrl string) ([]string, error) { // Extract cloud from credentials azureCloud, err := azcredentials.GetAzureCloud(settings, credentials) if err != nil { diff --git a/pkg/azuredx/adxauth/scopes_test.go b/pkg/azuredx/client/scopes_test.go similarity index 93% rename from pkg/azuredx/adxauth/scopes_test.go rename to pkg/azuredx/client/scopes_test.go index 5b0f25b2..b1965eba 100644 --- a/pkg/azuredx/adxauth/scopes_test.go +++ b/pkg/azuredx/client/scopes_test.go @@ -1,4 +1,4 @@ -package adxauth +package client import ( "testing" @@ -49,7 +49,7 @@ func TestGetAzureScopes_KnownClouds(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - scopes, err := getAzureScopes(settings, tt.credentials, tt.clusterUrl) + scopes, err := getAdxScopes(settings, tt.credentials, tt.clusterUrl) require.NoError(t, err) assert.Len(t, scopes, 1) @@ -67,7 +67,7 @@ func TestGetAzureScopes_UnknownClouds(t *testing.T) { credentials := &azcredentials.AzureClientSecretCredentials{AzureCloud: "Unknown"} clusterUrl := "https://abc.northeurope.unknown.net" - _, err := getAzureScopes(settings, credentials, clusterUrl) + _, err := getAdxScopes(settings, credentials, clusterUrl) assert.Error(t, err) }) } diff --git a/pkg/azuredx/datasource.go b/pkg/azuredx/datasource.go index a611ecef..ed021655 100644 --- a/pkg/azuredx/datasource.go +++ b/pkg/azuredx/datasource.go @@ -5,12 +5,10 @@ import ( "math/rand" "net/http" - "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth" "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth/adxcredentials" - "github.com/grafana/azure-data-explorer-datasource/pkg/azuredx/adxauth/adxusercontext" "github.com/grafana/grafana-azure-sdk-go/azsettings" + "github.com/grafana/grafana-azure-sdk-go/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" - sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" "github.com/grafana/grafana-plugin-sdk-go/data" @@ -59,24 +57,12 @@ func NewDatasource(instanceSettings backend.DataSourceInstanceSettings) (instanc credentials = adxcredentials.GetDefaultCredentials(azureSettings) } - httpClientOptions, err := instanceSettings.HTTPClientOptions() + adxClient, err := client.New(&instanceSettings, datasourceSettings, azureSettings, credentials) if err != nil { - backend.Logger.Error("failed to create HTTP client options", "error", err.Error()) + backend.Logger.Error("failed to create ADX client", "error", err.Error()) return nil, err } - httpClientOptions.Timeouts.Timeout = datasourceSettings.QueryTimeout - httpClient, err := sdkhttpclient.NewProvider(sdkhttpclient.ProviderOptions{}).New(httpClientOptions) - if err != nil { - backend.Logger.Error("failed to create HTTP client", "error", err.Error()) - return nil, err - } - - serviceCredentials, err := adxauth.NewServiceCredentials(datasourceSettings, azureSettings, credentials) - if err != nil { - return nil, err - } - - adx.client = client.New(serviceCredentials, httpClient) + adx.client = adxClient mux := http.NewServeMux() adx.registerRoutes(mux) @@ -87,7 +73,7 @@ func NewDatasource(instanceSettings backend.DataSourceInstanceSettings) (instanc // QueryData is the primary method called by grafana-server func (adx *AzureDataExplorer) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { - ctx = adxusercontext.WithUserFromQueryReq(ctx, req) + ctx = azusercontext.WithUserFromQueryReq(ctx, req) backend.Logger.Debug("Query", "datasource", req.PluginContext.DataSourceInstanceSettings.Name) res := backend.NewQueryDataResponse() @@ -100,12 +86,12 @@ func (adx *AzureDataExplorer) QueryData(ctx context.Context, req *backend.QueryD } func (adx *AzureDataExplorer) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { - return adx.CallResourceHandler.CallResource(adxusercontext.WithUserFromResourceReq(ctx, req), req, sender) + return adx.CallResourceHandler.CallResource(azusercontext.WithUserFromResourceReq(ctx, req), req, sender) } // CheckHealth handles health checks func (adx *AzureDataExplorer) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { - ctx = adxusercontext.WithUserFromHealthCheckReq(ctx, req) + ctx = azusercontext.WithUserFromHealthCheckReq(ctx, req) headers := map[string]string{} err := adx.client.TestRequest(ctx, adx.settings, models.NewConnectionProperties(adx.settings, nil), headers) if err != nil {