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

Add support for Azure authentication #785

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions .changeset/serious-badgers-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'grafana-infinity-datasource': minor
---

Add native Microsoft authentication
3 changes: 3 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ 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
- GF_AZURE_USER_IDENTITY_ENABLED=true
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was the reason for your error you should me at the review.

12 changes: 5 additions & 7 deletions docs/sources/examples/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@ 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 "Microsoft Entra ID"
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/<TENANT_ID>/oauth2/token`. Replace `<TENANT_ID>` 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. Add the Scope `https://management.azure.com/.default`.
7. 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
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22
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 v1.13.0
github.com/grafana/grafana-plugin-sdk-go v0.225.0
github.com/icholy/digest v0.1.22
github.com/stretchr/testify v1.9.0
Expand All @@ -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
Expand All @@ -47,6 +50,7 @@ require (
github.com/goccy/go-json v0.10.2 // indirect
github.com/goccy/go-yaml v1.11.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/flatbuffers v23.5.26+incompatible // indirect
github.com/google/go-cmp v0.6.0 // indirect
Expand All @@ -72,6 +76,7 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.17.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand Down Expand Up @@ -99,6 +104,7 @@ require (
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.18 // 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.18.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,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 v1.13.0 h1:2II2kXyHsBOCWkSQBYXrhhzuZpAn+viaesz3y+AyOSM=
github.com/grafana/grafana-azure-sdk-go v1.13.0/go.mod h1:SAlwLdEuox4vw8ZaeQwnepYXnhznnQQdstJbcw8LH68=
github.com/grafana/grafana-plugin-sdk-go v0.225.0 h1:m+mVt/MttHUmPFVTsOsAdYh5z+140Zkh1YIweDovr8Y=
github.com/grafana/grafana-plugin-sdk-go v0.225.0/go.mod h1:lpIXBVypJnOO2lAxkilRCan5zhTotPuDxMLmUMd9DAM=
github.com/grafana/sqlds/v3 v3.2.0 h1:WXuYEaFfiCvgm8kK2ixx44/zAEjFzCylA2+RF3GBqZA=
Expand Down
87 changes: 87 additions & 0 deletions pkg/infinity/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package infinity

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

"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-infinity-datasource/pkg/models"
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
)

func ApplyAzureAuth(ctx context.Context, httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) {
ctx, span := tracing.DefaultTracer().Start(ctx, "ApplyAzureAuth")
defer span.End()

if IsAzureAuthConfigured(settings) {
azSettings, err := azsettings.ReadFromEnv()
if err != nil {
return nil, err
}

var credentials azcredentials.AzureCredentials

switch settings.MicrosoftSettings.AuthType {
case models.MicrosoftAuthTypeClientSecret:

if strings.TrimSpace(settings.MicrosoftSettings.TenantID) == "" {
return nil, fmt.Errorf("Tenant ID %w ", models.MicrosoftRequiredForClientSecretErrHelp)
jkroepke marked this conversation as resolved.
Show resolved Hide resolved
}

if strings.TrimSpace(settings.MicrosoftSettings.ClientID) == "" {
return nil, fmt.Errorf("Client ID %w ", models.MicrosoftRequiredForClientSecretErrHelp)
jkroepke marked this conversation as resolved.
Show resolved Hide resolved
}

if strings.TrimSpace(settings.MicrosoftSettings.ClientSecret) == "" {
return nil, fmt.Errorf("Client secret %w ", models.MicrosoftRequiredForClientSecretErrHelp)
jkroepke marked this conversation as resolved.
Show resolved Hide resolved
}

credentials = &azcredentials.AzureClientSecretCredentials{
AzureCloud: string(settings.MicrosoftSettings.Cloud),
TenantId: settings.MicrosoftSettings.TenantID,
ClientId: settings.MicrosoftSettings.ClientID,
ClientSecret: settings.MicrosoftSettings.ClientSecret,
}
case models.MicrosoftAuthTypeManagedIdentity:
if !azSettings.ManagedIdentityEnabled {
return nil, fmt.Errorf("Managed Identity %w ", models.MicrosoftDisabledAuthErrHelp)
jkroepke marked this conversation as resolved.
Show resolved Hide resolved
}

credentials = &azcredentials.AzureManagedIdentityCredentials{
// ClientId is optional for managed identity, because it can be inferred from the environment
// https://github.com/grafana/grafana-azure-sdk-go/blob/21e2891b4190eb7c255c8cd275836def8200faf8/aztokenprovider/retriever_msi.go#L20-L30
ClientId: settings.MicrosoftSettings.ClientID,
}
case models.MicrosoftAuthTypeWorkloadIdentity:
if !azSettings.WorkloadIdentityEnabled {
return nil, fmt.Errorf("Workload Identity %w ", models.MicrosoftDisabledAuthErrHelp)
}

credentials = &azcredentials.AzureWorkloadIdentityCredentials{}
case models.MicrosoftAuthTypeCurrentUserIdentity:
if !azSettings.UserIdentityEnabled {
return nil, fmt.Errorf("User Identity %w ", models.MicrosoftDisabledAuthErrHelp)
jkroepke marked this conversation as resolved.
Show resolved Hide resolved
}

credentials = &azcredentials.AadCurrentUserCredentials{}
default:
panic(fmt.Errorf("invalid auth type '%s'", settings.MicrosoftSettings.AuthType))
}

authOpts := azhttpclient.NewAuthOptions(azSettings)
authOpts.Scopes(settings.MicrosoftSettings.Scopes)

httpClient.Transport = azhttpclient.AzureMiddleware(authOpts, credentials).
CreateMiddleware(httpclient.Options{}, httpClient.Transport)
}
return httpClient, nil
}

func IsAzureAuthConfigured(settings models.InfinitySettings) bool {
return settings.AuthenticationMethod == models.AuthenticationMethodMicrosoft
}
7 changes: 6 additions & 1 deletion pkg/infinity/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,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(httpClient, settings)
if err != nil {
Expand Down Expand Up @@ -155,7 +159,7 @@ func NewClient(ctx context.Context, settings models.InfinitySettings) (client *C
}

func ApplySecureSocksProxyConfiguration(httpClient *http.Client, settings models.InfinitySettings) (*http.Client, error) {
if IsAwsAuthConfigured(settings) {
if IsAwsAuthConfigured(settings) || IsAzureAuthConfigured(settings) {
return httpClient, nil
}
t := httpClient.Transport
Expand All @@ -173,6 +177,7 @@ func ApplySecureSocksProxyConfiguration(httpClient *http.Client, settings models
backend.Logger.Error("error configuring secure socks proxy", "err", err.Error())
return nil, fmt.Errorf("error configuring secure socks proxy. %s", err)
}

return httpClient, nil
}

Expand Down
86 changes: 86 additions & 0 deletions pkg/models/settings.go
jkroepke marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/textproto"
"strings"

"github.com/grafana/grafana-azure-sdk-go/azcredentials"
"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"
"golang.org/x/oauth2"
Expand All @@ -22,6 +24,7 @@ const (
AuthenticationMethodDigestAuth = "digestAuth"
AuthenticationMethodOAuth = "oauth2"
AuthenticationMethodAWS = "aws"
AuthenticationMethodMicrosoft = "microsoft"
AuthenticationMethodAzureBlob = "azureBlob"
)

Expand Down Expand Up @@ -62,6 +65,39 @@ type AWSSettings struct {
Service string `json:"service"`
}

type MicrosoftAuthType string

const (
MicrosoftAuthTypeManagedIdentity MicrosoftAuthType = azcredentials.AzureAuthManagedIdentity
MicrosoftAuthTypeWorkloadIdentity MicrosoftAuthType = azcredentials.AzureAuthWorkloadIdentity
MicrosoftAuthTypeClientSecret MicrosoftAuthType = azcredentials.AzureAuthClientSecret
MicrosoftAuthTypeCurrentUserIdentity MicrosoftAuthType = azcredentials.AzureAuthCurrentUserIdentity
)

type MicrosoftCloudType string

const (
MicrosoftCloudPublic MicrosoftCloudType = azsettings.AzurePublic
MicrosoftCloudChina MicrosoftCloudType = azsettings.AzureChina
MicrosoftCloudUSGovernment MicrosoftCloudType = azsettings.AzureUSGovernment
)

var (
MicrosoftRequiredForClientSecretErrHelp = errors.New(` is required for Microsoft client secret authentication`)
MicrosoftDisabledAuthErrHelp = errors.New(` is not enabled in the Grafana Azure settings. For more information, please refer to the Grafana documentation at
https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure.
Additionally, this plugin needs to be added to the grafana.ini setting azure.forward_settings_to_plugins.`)
)

type MicrosoftSettings struct {
Cloud MicrosoftCloudType `json:"cloud"`
AuthType MicrosoftAuthType `json:"auth_type"`
TenantID string `json:"tenant_id"`
ClientID string `json:"client_id"`
ClientSecret string
Scopes []string `json:"scopes,omitempty"`
}

type ProxyType string

const (
Expand Down Expand Up @@ -91,6 +127,7 @@ type InfinitySettings struct {
AWSSettings AWSSettings
AWSAccessKey string
AWSSecretKey string
MicrosoftSettings MicrosoftSettings
URL string
BasicAuthEnabled bool
UserName string
Expand Down Expand Up @@ -149,6 +186,42 @@ func (s *InfinitySettings) Validate() error {
}
return nil
}
if s.AuthenticationMethod == AuthenticationMethodMicrosoft {
azSettings, err := azsettings.ReadFromEnv()
if err != nil {
return err
}

switch s.MicrosoftSettings.AuthType {
case MicrosoftAuthTypeClientSecret:
if strings.TrimSpace(s.MicrosoftSettings.TenantID) == "" {
return fmt.Errorf("Tenant ID %w ", MicrosoftRequiredForClientSecretErrHelp)
}

if strings.TrimSpace(s.MicrosoftSettings.ClientID) == "" {
return fmt.Errorf("Client ID %w ", MicrosoftRequiredForClientSecretErrHelp)
}

if strings.TrimSpace(s.MicrosoftSettings.ClientSecret) == "" {
return fmt.Errorf("Client secret %w ", MicrosoftRequiredForClientSecretErrHelp)
}
case MicrosoftAuthTypeManagedIdentity:
if !azSettings.ManagedIdentityEnabled {
return errors.New("managed identity authentication is not enabled in Grafana config. " +
"Refer https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure")
}
case MicrosoftAuthTypeWorkloadIdentity:
if !azSettings.WorkloadIdentityEnabled {
return errors.New("workload identity authentication is not enabled in Grafana config." +
"Refer https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure")
}
case MicrosoftAuthTypeCurrentUserIdentity:
if !azSettings.UserIdentityEnabled {
return errors.New("user identity authentication is not enabled in Grafana config." +
"Refer https://grafana.com/docs/grafana/latest/setup-grafana/configure-grafana/#azure")
}
}
}
if s.AuthenticationMethod != AuthenticationMethodNone && len(s.AllowedHosts) < 1 {
return errors.New("configure allowed hosts in the authentication section")
}
Expand Down Expand Up @@ -256,6 +329,16 @@ func LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings
if len(infJson.AllowedHosts) > 0 {
settings.AllowedHosts = infJson.AllowedHosts
}

settings.MicrosoftSettings = infJson.MicrosoftSettings
if settings.AuthenticationMethod == "microsoft" {
if settings.MicrosoftSettings.AuthType == "" {
settings.MicrosoftSettings.AuthType = "clientsecret"
}
if settings.MicrosoftSettings.Cloud == "" {
settings.MicrosoftSettings.Cloud = MicrosoftCloudPublic
}
}
}
settings.ReferenceData = infJson.ReferenceData
settings.CustomHealthCheckEnabled = infJson.CustomHealthCheckEnabled
Expand Down Expand Up @@ -292,6 +375,9 @@ func LoadSettings(ctx context.Context, config backend.DataSourceInstanceSettings
if val, ok := config.DecryptedSecureJSONData["azureBlobAccountKey"]; ok {
settings.AzureBlobAccountKey = val
}
if val, ok := config.DecryptedSecureJSONData["microsoftClientSecret"]; ok {
settings.MicrosoftSettings.ClientSecret = val
}
settings.CustomHeaders = GetSecrets(config, "httpHeaderName", "httpHeaderValue")
settings.SecureQueryFields = GetSecrets(config, "secureQueryName", "secureQueryValue")
settings.OAuth2Settings.EndpointParams = GetSecrets(config, "oauth2EndPointParamsName", "oauth2EndPointParamsValue")
Expand Down
16 changes: 16 additions & 0 deletions pkg/models/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) {
"region" : "region1",
"service" : "service1"
},
"microsoft" : {
"cloud" : "AzureUSGovernment",
"auth_type" : "clientsecret",
"tenant_id" : "tenant1",
"client_id" : "myMicrosoftClientID",
"scopes" : ["msscope1","msscope2"]
},
"oauth2" : {
"client_id":"myClientID",
"email":"myEmail",
Expand All @@ -184,6 +191,7 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) {
"awsAccessKey": "awsAccessKey1",
"awsSecretKey": "awsSecretKey1",
"oauth2ClientSecret": "myOauth2ClientSecret",
"microsoftClientSecret": "myMicrosoftClientSecret",
"oauth2JWTPrivateKey": "myOauth2JWTPrivateKey",
"oauth2EndPointParamsValue1": "Resource1",
"oauth2EndPointParamsValue2": "Resource2",
Expand Down Expand Up @@ -215,6 +223,14 @@ func TestAllSettingsAgainstFrontEnd(t *testing.T) {
Service: "service1",
Region: "region1",
},
MicrosoftSettings: models.MicrosoftSettings{
Cloud: models.MicrosoftCloudUSGovernment,
AuthType: models.MicrosoftAuthTypeClientSecret,
TenantID: "tenant1",
ClientID: "myMicrosoftClientID",
ClientSecret: "myMicrosoftClientSecret",
Scopes: []string{"msscope1", "msscope2"},
},
OAuth2Settings: models.OAuth2Settings{
ClientID: "myClientID",
OAuth2Type: "client_credentials",
Expand Down
Loading