diff --git a/.changeset/serious-badgers-fetch.md b/.changeset/serious-badgers-fetch.md new file mode 100644 index 000000000..b45a1e922 --- /dev/null +++ b/.changeset/serious-badgers-fetch.md @@ -0,0 +1,5 @@ +--- +'grafana-infinity-datasource': minor +--- + +Add native Azure authentication diff --git a/cspell.config.json b/cspell.config.json index fc6100d9b..c8a252172 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -26,6 +26,9 @@ "words": [ "ANSIC", "azblob", + "azcredentials", + "azhttpclient", + "azsettings", "basgys", "Bauch", "bigpanda", @@ -33,6 +36,7 @@ "builtins", "Chelsey", "clientcredentials", + "clientsecret", "clsx", "cmdk", "Collapsable", @@ -85,6 +89,7 @@ "lucide", "magefile", "mainbg", + "maputil", "maxif", "microsocks", "Milli", @@ -170,6 +175,7 @@ "visualisation", "vlookup", "vuepress", + "workloadidentity", "xinsnake", "xmlframer", "yesoreyeram", diff --git a/docker-compose.yaml b/docker-compose.yaml index ec84914e4..cc9eccb28 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -22,3 +22,5 @@ services: - GF_SECURITY_ANGULAR_SUPPORT_ENABLED=false - GF_SECURITY_CSRF_ALWAYS_CHECK=true - GF_ENTERPRISE_LICENSE_TEXT=$GF_ENTERPRISE_LICENSE_TEXT + - GF_AZURE_FORWARD_SETTINGS_TO_PLUGINS= + - GF_AZURE_WORKLOAD_IDENTITY_ENABLED=true diff --git a/docs/sources/examples/azure.md b/docs/sources/examples/azure.md index d29b1c1f2..35d127abb 100644 --- a/docs/sources/examples/azure.md +++ b/docs/sources/examples/azure.md @@ -31,15 +31,12 @@ Here are the detailed steps on how to connect Microsoft Azure APIs 3. Note down the Client ID, Client Secret and Tenant ID 4. Give reader/monitoring reader access to the resources/subscriptions as necessary 5. Install the infinity plugin in Grafana and add data source for the same - 1. Expand Authentication section and select "OAuth2" - 2. Select "Client Credentials" as OAuth2 type + 1. Expand Authentication section and select "Azure" + 2. Select "Client Credentials" as Auth type 3. Specify the Client ID 4. Specify the Client Secret - 5. Specify the Token URL `https://login.microsoftonline.com//oauth2/token`. Replace `` with yours - 6. Leave the Scopes section empty - 7. Add the following Endpoint param - 1. Key : `resource` Value: `https://management.azure.com/` - 8. If you are using Infinity 1.0.0+, then also specify `https://management.azure.com/` as an allowed URL. + 5. Specify the Tenant ID + 6. If you are using Infinity 1.0.0+, then also specify `https://management.azure.com/` as an allowed URL. 6. Click Save and Test. 7. Click the `Explore` button 8. Configure the query diff --git a/go.mod b/go.mod index 612a4b642..472c19a60 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.1 require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1 github.com/grafana/grafana-aws-sdk v0.24.0 + github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 github.com/grafana/grafana-plugin-sdk-go v0.241.0 github.com/grafana/infinity-libs/lib/go/csvframer v1.0.0 github.com/grafana/infinity-libs/lib/go/gframer v1.0.0 @@ -22,7 +23,9 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/apache/arrow/go/v15 v15.0.2 // indirect github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect @@ -44,6 +47,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -68,6 +72,7 @@ require ( github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattetti/filebuffer v1.0.1 // indirect @@ -89,6 +94,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect diff --git a/go.sum b/go.sum index a22fcfe85..13d9ede19 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= @@ -97,6 +97,8 @@ github.com/grafana/dataplane/sdata v0.0.7 h1:CImITypIyS1jxijCR6xqKx71JnYAxcwpH9C github.com/grafana/dataplane/sdata v0.0.7/go.mod h1:Jvs5ddpGmn6vcxT7tCTWAZ1mgi4sbcdFt9utQx5uMAU= github.com/grafana/grafana-aws-sdk v0.24.0 h1:0RKCJTeIkpEUvLCTjGOK1+jYZpaE2nJaGghGLvtUsFs= github.com/grafana/grafana-aws-sdk v0.24.0/go.mod h1:3zghFF6edrxn0d6k6X9HpGZXDH+VfA+MwD2Pc/9X0ec= +github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 h1:lajVqTWaE96MpbjZToj7EshvqgRWOfYNkD4MbIZizaY= +github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0/go.mod h1:aKlFPE36IDa8qccRg3KbgZX3MQ5xymS3RelT4j6kkVU= github.com/grafana/grafana-plugin-sdk-go v0.241.0 h1:zBcSW9xV9gA9hD8UN+HjJtD7tESMZcaQhA1BI76MTxM= github.com/grafana/grafana-plugin-sdk-go v0.241.0/go.mod h1:2HjNwzGCfaFAyR2HGoECTwAmq8vSIn2L1/1yOt4XRS4= github.com/grafana/infinity-libs/lib/go/csvframer v1.0.0 h1:PGM6BkwU1You9zFShVTtNvQcnQDabJ4jg9TLhqAdC/k= diff --git a/pkg/infinity/azure.go b/pkg/infinity/azure.go new file mode 100644 index 000000000..0ccab9f13 --- /dev/null +++ b/pkg/infinity/azure.go @@ -0,0 +1,87 @@ +package infinity + +import ( + "context" + "fmt" + "net/http" + + "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/v2/azhttpclient" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" + "github.com/grafana/grafana-plugin-sdk-go/data/utils/maputil" + + "github.com/grafana/grafana-infinity-datasource/pkg/models" +) + +func ApplyAzureAuth(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { + _, span := tracing.DefaultTracer().Start(ctx, "ApplyAzureAuth") + defer span.End() + + if !IsAzureAuthConfigured(settings) { + return httpClient, nil + } + + credentialsObj, err := maputil.GetMapOptional(settings.RawData, "azureCredentials") + if err != nil { + return nil, err + } else if credentialsObj == nil { + return httpClient, nil + } + + azSettings, err := azsettings.ReadFromEnv() + if err != nil { + return nil, err + } + + // set authType if not set + // this can happen if the user manually provisions the datasource without setting the authType + if credentialsObj["authType"] == nil || credentialsObj["authType"] == "" { + credentialsObj["authType"] = azcredentials.AzureAuthClientSecret + } + + // set azureCloud if not set + // this can happen if the user manually provisions the datasource without setting the azureCloud + if credentialsObj["azureCloud"] == nil || credentialsObj["azureCloud"] == "" { + credentialsObj["azureCloud"] = azSettings.GetDefaultCloud() + } + + azCredentials, err := azcredentials.FromDatasourceData(settings.RawData, settings.RawSecureData) + if err != nil { + return nil, err + } + + authOpts := azhttpclient.NewAuthOptions(azSettings) + + // set scopes + if scopes, ok := credentialsObj["scopes"].([]any); ok { + stringScopes, err := toStringSlice(scopes) + if err != nil { + return nil, err + } + + authOpts.Scopes(stringScopes) + } + + httpClient.Transport = azhttpclient.AzureMiddleware(authOpts, azCredentials). + CreateMiddleware(httpclient.Options{}, httpClient.Transport) + + return httpClient, nil +} + +func IsAzureAuthConfigured(settings models.InfinitySettings) bool { + return settings.AuthenticationMethod == models.AuthenticationMethodAzure +} + +func toStringSlice(arr []any) ([]string, error) { + result := make([]string, len(arr)) + for i, v := range arr { + if s, ok := v.(string); ok { + result[i] = s + } else { + return nil, fmt.Errorf("expected string, got %T", v) + } + } + return result, nil +} diff --git a/pkg/infinity/client.go b/pkg/infinity/client.go index 071fc5911..6d7a3fda2 100644 --- a/pkg/infinity/client.go +++ b/pkg/infinity/client.go @@ -109,6 +109,10 @@ func NewClient(ctx context.Context, settings models.InfinitySettings) (client *C httpClient = ApplyOAuthClientCredentials(ctx, httpClient, settings) httpClient = ApplyOAuthJWT(ctx, httpClient, settings) httpClient = ApplyAWSAuth(ctx, httpClient, settings) + httpClient, err = ApplyAzureAuth(ctx, httpClient, settings) + if err != nil { + return nil, err + } httpClient, err = ApplySecureSocksProxyConfiguration(ctx, httpClient, settings) if err != nil { @@ -159,7 +163,7 @@ func NewClient(ctx context.Context, settings models.InfinitySettings) (client *C func ApplySecureSocksProxyConfiguration(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) { logger := backend.Logger.FromContext(ctx) - if IsAwsAuthConfigured(settings) { + if IsAwsAuthConfigured(settings) || IsAzureAuthConfigured(settings) { return httpClient, nil } t := httpClient.Transport diff --git a/pkg/models/settings.go b/pkg/models/settings.go index e6ffd04d2..7bcaef797 100644 --- a/pkg/models/settings.go +++ b/pkg/models/settings.go @@ -8,6 +8,8 @@ import ( "net/textproto" "strings" + "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" + "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "golang.org/x/oauth2" @@ -22,6 +24,7 @@ const ( AuthenticationMethodDigestAuth = "digestAuth" AuthenticationMethodOAuth = "oauth2" AuthenticationMethodAWS = "aws" + AuthenticationMethodAzure = "azure" AuthenticationMethodAzureBlob = "azureBlob" ) @@ -119,6 +122,9 @@ type InfinitySettings struct { PathEncodedURLsEnabled bool // ProxyOpts is used for Secure Socks Proxy configuration ProxyOpts httpclient.Options + + RawData map[string]interface{} + RawSecureData map[string]string } func (s *InfinitySettings) Validate() error { @@ -149,6 +155,17 @@ func (s *InfinitySettings) Validate() error { } return nil } + if s.AuthenticationMethod == AuthenticationMethodAzure { + _, err := azsettings.ReadFromEnv() + if err != nil { + return err + } + + _, err = azcredentials.FromDatasourceData(s.RawData, s.RawSecureData) + if err != nil { + return err + } + } if s.AuthenticationMethod != AuthenticationMethodNone && len(s.AllowedHosts) < 1 { return errors.New("configure allowed hosts in the authentication section") } @@ -256,6 +273,15 @@ func LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings if len(infJson.AllowedHosts) > 0 { settings.AllowedHosts = infJson.AllowedHosts } + + var rawData map[string]interface{} + + if err := json.Unmarshal(config.JSONData, &rawData); err != nil { + return settings, err + } + + settings.RawData = rawData + settings.RawSecureData = config.DecryptedSecureJSONData } settings.ReferenceData = infJson.ReferenceData settings.CustomHealthCheckEnabled = infJson.CustomHealthCheckEnabled @@ -292,6 +318,7 @@ func LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings if val, ok := config.DecryptedSecureJSONData["azureBlobAccountKey"]; ok { settings.AzureBlobAccountKey = val } + settings.CustomHeaders = GetSecrets(config, "httpHeaderName", "httpHeaderValue") settings.SecureQueryFields = GetSecrets(config, "secureQueryName", "secureQueryValue") settings.OAuth2Settings.EndpointParams = GetSecrets(config, "oauth2EndPointParamsName", "oauth2EndPointParamsValue") diff --git a/pkg/models/settings_test.go b/pkg/models/settings_test.go index 8019028dc..ea4795090 100644 --- a/pkg/models/settings_test.go +++ b/pkg/models/settings_test.go @@ -72,6 +72,16 @@ func TestLoadSettings(t *testing.T) { SecureQueryFields: map[string]string{ "foo": "bar", }, + RawData: map[string]interface{}{ + "datasource_mode": "advanced", + "httpHeaderName1": "header1", + "secureQueryName1": "foo", + }, + RawSecureData: map[string]string{ + "basicAuthPassword": "password", + "httpHeaderValue1": "headervalue1", + "secureQueryValue1": "bar", + }, }, }, { @@ -111,6 +121,17 @@ func TestLoadSettings(t *testing.T) { SecureQueryFields: map[string]string{ "foo": "bar", }, + RawData: map[string]interface{}{ + "datasource_mode": "advanced", + "httpHeaderName1": "header1", + "secureQueryName1": "foo", + "timeoutInSeconds": float64(30), + }, + RawSecureData: map[string]string{ + "basicAuthPassword": "password", + "httpHeaderValue1": "headervalue1", + "secureQueryValue1": "bar", + }, }, }, } @@ -184,6 +205,7 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) { "awsAccessKey": "awsAccessKey1", "awsSecretKey": "awsSecretKey1", "oauth2ClientSecret": "myOauth2ClientSecret", + "azureClientSecret": "myMicrosoftClientSecret", "oauth2JWTPrivateKey": "myOauth2JWTPrivateKey", "oauth2EndPointParamsValue1": "Resource1", "oauth2EndPointParamsValue2": "Resource2", @@ -250,6 +272,63 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) { SecureQueryFields: map[string]string{ "foo": "bar", }, + RawData: map[string]interface{}{ + "allowedHosts": []interface{}{ + "host1", + "host2", + }, + "apiKeyKey": "hello", + "apiKeyType": "query", + "auth_method": "oauth2", + "aws": map[string]interface{}{ + "authType": "keys", + "region": "region1", + "service": "service1", + }, + "customHealthCheckEnabled": true, + "customHealthCheckUrl": "https://foo-check/", + "datasource_mode": "advanced", + "httpHeaderName1": "header1", + "oauth2": map[string]interface{}{ + "client_id": "myClientID", + "email": "myEmail", + "private_key_id": "saturn", + "scopes": []interface{}{ + "scope1", + "scope2", + }, + "subject": "mySubject", + "token_url": "TOKEN_URL", + }, + "oauth2EndPointParamsName1": "resource", + "oauth2EndPointParamsName2": "name", + "oauthPassThru": true, + "proxy_type": "url", + "proxy_url": "https://foo.com", + "secureQueryName1": "foo", + "timeoutInSeconds": float64(30), + "tlsAuth": true, + "tlsAuthWithCACert": true, + "tlsSkipVerify": true, + "unsecuredQueryHandling": "deny", + }, + RawSecureData: map[string]string{ + "apiKeyValue": "earth", + "awsAccessKey": "awsAccessKey1", + "awsSecretKey": "awsSecretKey1", + "azureClientSecret": "myMicrosoftClientSecret", + "basicAuthPassword": "password", + "bearerToken": "myBearerToken", + "httpHeaderValue1": "headervalue1", + "oauth2ClientSecret": "myOauth2ClientSecret", + "oauth2EndPointParamsValue1": "Resource1", + "oauth2EndPointParamsValue2": "Resource2", + "oauth2JWTPrivateKey": "myOauth2JWTPrivateKey", + "secureQueryValue1": "bar", + "tlsCACert": "myTlsCACert", + "tlsClientCert": "myTlsClientCert", + "tlsClientKey": "myTlsClientKey", + }, }, gotSettings) } diff --git a/src/editors/config/Auth.tsx b/src/editors/config/Auth.tsx index d0bdd9fbe..4291368b3 100644 --- a/src/editors/config/Auth.tsx +++ b/src/editors/config/Auth.tsx @@ -4,6 +4,7 @@ import { Icon, InlineFormLabel, LegacyForms, RadioButtonGroup, Select, useTheme2 import React, { useState } from 'react'; import { AllowedHostsEditor } from './AllowedHosts'; import { OAuthInputsEditor } from './OAuthInput'; +import { AzureInputsEditor } from './AzureInput'; import { OthersAuthentication } from './OtherAuthProviders'; import { AWSRegions } from './../../constants'; import type { APIKeyType, AuthType, InfinityOptions, InfinitySecureOptions } from './../../types'; @@ -17,6 +18,7 @@ const authTypes: Array & { logo?: string }> { value: 'oauthPassThru', label: 'Forward OAuth' }, { value: 'oauth2', label: 'OAuth2', logo: '/public/plugins/yesoreyeram-infinity-datasource/img/oauth-2-sm.png' }, { value: 'aws', label: 'AWS', logo: '/public/plugins/yesoreyeram-infinity-datasource/img/aws.jpg' }, + { value: 'azure', label: 'Azure', logo: '/public/img/microsoft_auth_icon.svg' }, { value: 'azureBlob', label: 'Azure Blob' }, { value: 'others', label: 'Other Auth Providers' }, ]; @@ -76,6 +78,7 @@ export const AuthEditor = (props: DataSourcePluginOptionsEditorProps - {a.logo ? : } + {a.logo ? : } {a.label} ))} @@ -276,6 +279,7 @@ export const AuthEditor = (props: DataSourcePluginOptionsEditorProps )} {authType === 'oauth2' && } + {authType === 'azure' && } {authType === 'azureBlob' && ( <>
diff --git a/src/editors/config/AzureInput.tsx b/src/editors/config/AzureInput.tsx new file mode 100644 index 000000000..ac0b7f4e9 --- /dev/null +++ b/src/editors/config/AzureInput.tsx @@ -0,0 +1,139 @@ +import { onUpdateDatasourceSecureJsonDataOption, DataSourcePluginOptionsEditorProps, SelectableValue } from '@grafana/data'; +import {InlineFormLabel, Input, LegacyForms, RadioButtonGroup, Select} from '@grafana/ui'; +import React from 'react'; +import type { + InfinityOptions, + InfinitySecureOptions, + AzureAuthType, AzureCloudType, + AzureProps +} from './../../types'; + +const azureCloudTypes: Array> = [ + { value: 'AzureCloud', label: 'Azure' }, + { value: 'AzureUSGovernment', label: 'Azure US Government' }, + { value: 'AzureChinaCloud', label: 'Azure China Cloud' }, +]; + +const azureAuthTypes: Array> = [ + { value: 'clientsecret', label: 'Client Secret' }, + { value: 'msi', label: 'Managed Identity' }, + { value: 'workloadidentity', label: 'Workload Identity' }, +]; + +export const AzureInputsEditor = (props: DataSourcePluginOptionsEditorProps) => { + const { options, onOptionsChange } = props; + const { secureJsonFields } = options; + const secureJsonData = (options.secureJsonData || {}) as InfinitySecureOptions; + let azure: AzureProps = options?.jsonData?.azureCredentials || {}; + + const onAzurePropsChange = (key: T, value: V) => { + onOptionsChange({ ...options, jsonData: { ...options.jsonData, azureCredentials: { ...azure, [key]: value } } }); + }; + + const onResetClientSecret = () => { + onOptionsChange({ + ...options, + secureJsonFields: { ...options.secureJsonFields, azureClientSecret: false }, + secureJsonData: { ...options.secureJsonData, azureClientSecret: '' }, + }); + }; + return ( + <> +
+ + Cloud + + +
+
+ + {`Managed Identity, Workload Identity requires ambient credentials. `} + {`An Grafana admin has to explicit opt-in via `} + grafana.ini + {` to allow the authentication methods.`} +
+
+ {`For more information, please refer to the `} + Grafana documentation. +
+
+ {`Additionally, this plugin needs to be added to the grafana.ini setting `} + azure.forward_settings_to_plugins. +
+                    [azure]
+                    {`\nforward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql, yesoreyeram-infinity-datasource`}
+                    
+ + } + {...{interactive: true }}> + Authentication Type +
+ + options={azureAuthTypes} + onChange={(v) => onAzurePropsChange('authType', v)} + value={azure.authType || 'clientsecret'}> +
+ {(azure.authType !== 'msi' || !azure?.authType) && ( + <> +
+ Tenant ID + onAzurePropsChange('tenantId', v.currentTarget.value)} value={azure.tenantId} width={60} placeholder={'Tenant ID'} /> +
+ + )} + {(azure.authType !== 'msi' || !azure?.authType) && ( +
+ Client ID + onAzurePropsChange('clientId', v.currentTarget.value)} value={azure.clientId} width={60} placeholder={'Client ID'} /> +
+ )} + {(azure.authType === 'clientsecret' || !azure?.authType) && ( + <> +
+ +
+ + )} +
+ + {`For accessing the Microsoft Azure Resource Manager, use https://management.azure.com/.default`} +
+
+ {`For accessing the Microsoft Graph API, use: https://graph.microsoft.com/.default`} +
+
+ {`For accessing other API, use: https://[hostname]/.default`} + + } + {...{interactive: true }} + >Scopes
+ onAzurePropsChange('scopes', (v.currentTarget.value || '').split(','))} + value={(azure.scopes || []).join(',')} + width={60} + placeholder={'Comma separated values of scopes'} + /> +
+ + ); +}; diff --git a/src/types/config.types.ts b/src/types/config.types.ts index c03956e2a..ba6dba46f 100644 --- a/src/types/config.types.ts +++ b/src/types/config.types.ts @@ -7,7 +7,7 @@ export interface GlobalInfinityQuery { id: string; query: InfinityQuery; } -export type AuthType = 'none' | 'basicAuth' | 'apiKey' | 'bearerToken' | 'oauthPassThru' | 'digestAuth' | 'aws' | 'azureBlob' | 'oauth2'; +export type AuthType = 'none' | 'basicAuth' | 'apiKey' | 'bearerToken' | 'oauthPassThru' | 'digestAuth' | 'aws' | 'azureBlob' | 'oauth2' | 'azure'; export type OAuth2Type = 'client_credentials' | 'jwt' | 'others'; export type APIKeyType = 'header' | 'query'; export type OAuth2Props = { @@ -25,6 +25,18 @@ export type AWSAuthProps = { region?: string; service?: string; }; + +export type AzureCloudType = 'AzureCloud' | 'AzureChinaCloud' | 'AzureUSGovernment'; +export type AzureAuthType = 'clientsecret' | 'msi' | 'workloadidentity'; + +// the keys are used to align with the Grafana Azure SDK +export type AzureProps = { + azureCloud?: AzureCloudType; + authType?: AzureAuthType; + tenantId?: string; + clientId?: string; + scopes?: string[]; +}; export type InfinityReferenceData = { name: string; data: string }; export type ProxyType = 'none' | 'env' | 'url'; export type UnsecureQueryHandling = 'warn' | 'allow' | 'deny'; @@ -34,6 +46,7 @@ export interface InfinityOptions extends DataSourceJsonData { apiKeyType?: APIKeyType; oauth2?: OAuth2Props; aws?: AWSAuthProps; + azureCredentials?: AzureProps; tlsSkipVerify?: boolean; tlsAuth?: boolean; serverName?: string; @@ -64,6 +77,7 @@ export interface InfinitySecureOptions { awsAccessKey?: string; awsSecretKey?: string; oauth2ClientSecret?: string; + azureClientSecret?: string; oauth2JWTPrivateKey?: string; azureBlobAccountKey?: string; }