From eafba8fa69ad9e73bfc39eaf3718df755e378e5d Mon Sep 17 00:00:00 2001 From: Sergey Kostrukov Date: Mon, 15 May 2023 10:00:54 -0700 Subject: [PATCH] Azure: Configuration for user identity authentication in datasources (Experimental) (#50277) * Configuration for user identity authentication * Use token endpoint form Azure AD settings * Documentation update * Update Grafana Azure SDK * Fix secret override * Fix lint * Fix doc wording --- conf/defaults.ini | 17 ++ conf/sample.ini | 17 ++ .../setup-grafana/configure-grafana/_index.md | 24 +++ go.mod | 2 +- go.sum | 4 +- packages/grafana-runtime/src/config.ts | 2 + pkg/api/dtos/frontend_settings.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/api/pluginproxy/token_provider_azure.go | 2 +- pkg/setting/setting_azure.go | 34 +++- pkg/setting/setting_azure_test.go | 152 ++++++++++++++++++ 11 files changed, 250 insertions(+), 6 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 261d7e45148..c8a98567212 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -808,6 +808,23 @@ managed_identity_enabled = false # Should be set for user-assigned identity and should be empty for system-assigned identity managed_identity_client_id = +# Specifies whether user identity authentication (on behalf of currently signed-in user) should be enabled in datasources +# that support it (requires AAD authentication) +# Disabled by default, needs to be explicitly enabled +user_identity_enabled = false + +# Override token URL for Azure Active Directory +# By default is the same as token URL configured for AAD authentication settings +user_identity_token_url = + +# Override ADD application ID which would be used to exchange users token to an access token for the datasource +# By default is the same as used in AAD authentication or can be set to another application (for OBO flow) +user_identity_client_id = + +# Override the AAD application client secret +# By default is the same as used in AAD authentication or can be set to another application (for OBO flow) +user_identity_client_secret = + #################################### Role-based Access Control ########### [rbac] # If enabled, cache permissions in a in memory cache diff --git a/conf/sample.ini b/conf/sample.ini index 26eb48cf244..416195cd14d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -780,6 +780,23 @@ # Should be set for user-assigned identity and should be empty for system-assigned identity ;managed_identity_client_id = +# Specifies whether user identity authentication (on behalf of currently signed-in user) should be enabled in datasources +# that support it (requires AAD authentication) +# Disabled by default, needs to be explicitly enabled +;user_identity_enabled = false + +# Override token URL for Azure Active Directory +# By default is the same as token URL configured for AAD authentication settings +;user_identity_token_url = + +# Override ADD application ID which would be used to exchange users token to an access token for the datasource +# By default is the same as used in AAD authentication or can be set to another application (for OBO flow) +;user_identity_client_id = + +# Override the AAD application client secret +# By default is the same as used in AAD authentication or can be set to another application (for OBO flow) +;user_identity_client_secret = + #################################### Role-based Access Control ########### [rbac] ;permission_cache = true diff --git a/docs/sources/setup-grafana/configure-grafana/_index.md b/docs/sources/setup-grafana/configure-grafana/_index.md index bf807529227..be625b0b173 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1117,6 +1117,30 @@ The client ID to use for user-assigned managed identity. Should be set for user-assigned identity and should be empty for system-assigned identity. +### user_identity_enabled + +Specifies whether user identity authentication (on behalf of currently signed-in user) should be enabled in datasources that support it (requires AAD authentication). + +Disabled by default, needs to be explicitly enabled. + +### user_identity_token_url + +Override token URL for Azure Active Directory. + +By default is the same as token URL configured for AAD authentication settings. + +### user_identity_client_id + +Override ADD application ID which would be used to exchange users token to an access token for the datasource. + +By default is the same as used in AAD authentication or can be set to another application (for OBO flow). + +### user_identity_client_secret + +Override the AAD application client secret. + +By default is the same as used in AAD authentication or can be set to another application (for OBO flow). + ## [auth.jwt] Refer to [JWT authentication]({{< relref "../configure-security/configure-authentication/jwt/" >}}) for more information. diff --git a/go.mod b/go.mod index d211c2ebc9a..f07e2474f33 100644 --- a/go.mod +++ b/go.mod @@ -60,7 +60,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/grafana/alerting v0.0.0-20230428095912-33c5aa68a5ba github.com/grafana/grafana-aws-sdk v0.15.0 - github.com/grafana/grafana-azure-sdk-go v1.6.0 + github.com/grafana/grafana-azure-sdk-go v1.7.0 github.com/grafana/grafana-plugin-sdk-go v0.160.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/hashicorp/go-hclog v1.5.0 diff --git a/go.sum b/go.sum index 0dd280a52ab..bd5430000c9 100644 --- a/go.sum +++ b/go.sum @@ -1054,8 +1054,8 @@ github.com/grafana/go-mssqldb v0.9.2 h1:FkyRJR4ywsT07iMtpFMHStrl8uuNkGIwp253Fee0 github.com/grafana/go-mssqldb v0.9.2/go.mod h1:HTCsUqZdb7oIO7jc37YauiSB5C3P/13AnpctVWBhlus= github.com/grafana/grafana-aws-sdk v0.15.0 h1:ZOPHQcC5NUFi1bLTwnju91G0KmGh1z+qXOKj9nDfxNs= github.com/grafana/grafana-aws-sdk v0.15.0/go.mod h1:rCXLYoMpPqF90U7XqgVJ1HIAopFVF0bB3SXBVEJIm3I= -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-azure-sdk-go v1.7.0 h1:2EAPwNl/qsDMHwKjlzaHif+H+bHcF1W7sM8/jAcxVcI= +github.com/grafana/grafana-azure-sdk-go v1.7.0/go.mod h1:X4PdEQIYgHfn0KTa2ZTKvufhNz6jbCEKUQPZIlcyOGw= github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HGQbTqewE3JvRaNA= github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE= github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M= diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index d4dc6088f4c..f21b548ce33 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -21,6 +21,7 @@ import { export interface AzureSettings { cloud?: string; managedIdentityEnabled: boolean; + userIdentityEnabled: boolean; } export type AppPluginConfig = { @@ -121,6 +122,7 @@ export class GrafanaBootConfig implements GrafanaConfig { awsAssumeRoleEnabled = false; azure: AzureSettings = { managedIdentityEnabled: false, + userIdentityEnabled: false, }; caching = { enabled: false, diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index cf46d35bbed..770486f5798 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -46,6 +46,7 @@ type FrontendSettingsLicenseInfoDTO struct { type FrontendSettingsAzureDTO struct { Cloud string `json:"cloud"` ManagedIdentityEnabled bool `json:"managedIdentityEnabled"` + UserIdentityEnabled bool `json:"userIdentityEnabled"` } type FrontendSettingsCachingDTO struct { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 2791d5e272c..9992363821d 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -198,6 +198,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro Azure: dtos.FrontendSettingsAzureDTO{ Cloud: hs.Cfg.Azure.Cloud, ManagedIdentityEnabled: hs.Cfg.Azure.ManagedIdentityEnabled, + UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled, }, Caching: dtos.FrontendSettingsCachingDTO{ diff --git a/pkg/api/pluginproxy/token_provider_azure.go b/pkg/api/pluginproxy/token_provider_azure.go index a0db3524c35..c2ef22bfe8d 100644 --- a/pkg/api/pluginproxy/token_provider_azure.go +++ b/pkg/api/pluginproxy/token_provider_azure.go @@ -20,7 +20,7 @@ type azureAccessTokenProvider struct { func newAzureAccessTokenProvider(ctx context.Context, cfg *setting.Cfg, authParams *plugins.JWTTokenAuth) (*azureAccessTokenProvider, error) { credentials := getAzureCredentials(cfg.Azure, authParams) - tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg.Azure, credentials) + tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg.Azure, credentials, false) if err != nil { return nil, err } diff --git a/pkg/setting/setting_azure.go b/pkg/setting/setting_azure.go index 0a6d5cc4022..05fb415ad9f 100644 --- a/pkg/setting/setting_azure.go +++ b/pkg/setting/setting_azure.go @@ -1,6 +1,8 @@ package setting -import "github.com/grafana/grafana-azure-sdk-go/azsettings" +import ( + "github.com/grafana/grafana-azure-sdk-go/azsettings" +) func (cfg *Cfg) readAzureSettings() { azureSettings := &azsettings.AzureSettings{} @@ -11,9 +13,37 @@ func (cfg *Cfg) readAzureSettings() { cloudName := azureSection.Key("cloud").MustString(azsettings.AzurePublic) azureSettings.Cloud = azsettings.NormalizeAzureCloud(cloudName) - // Managed Identity + // Managed Identity authentication azureSettings.ManagedIdentityEnabled = azureSection.Key("managed_identity_enabled").MustBool(false) azureSettings.ManagedIdentityClientId = azureSection.Key("managed_identity_client_id").String() + // User Identity authentication + if azureSection.Key("user_identity_enabled").MustBool(false) { + azureSettings.UserIdentityEnabled = true + tokenEndpointSettings := &azsettings.TokenEndpointSettings{} + + // Get token endpoint from Azure AD settings if enabled + azureAdSection := cfg.Raw.Section("auth.azuread") + if azureAdSection.Key("enabled").MustBool(false) { + tokenEndpointSettings.TokenUrl = azureAdSection.Key("token_url").String() + tokenEndpointSettings.ClientId = azureAdSection.Key("client_id").String() + tokenEndpointSettings.ClientSecret = azureAdSection.Key("client_secret").String() + } + + // Override individual settings + if val := azureSection.Key("user_identity_token_url").String(); val != "" { + tokenEndpointSettings.TokenUrl = val + } + if val := azureSection.Key("user_identity_client_id").String(); val != "" { + tokenEndpointSettings.ClientId = val + tokenEndpointSettings.ClientSecret = "" + } + if val := azureSection.Key("user_identity_client_secret").String(); val != "" { + tokenEndpointSettings.ClientSecret = val + } + + azureSettings.UserIdentityTokenEndpoint = tokenEndpointSettings + } + cfg.Azure = azureSettings } diff --git a/pkg/setting/setting_azure_test.go b/pkg/setting/setting_azure_test.go index 7df70e823b4..a7ec9df6f98 100644 --- a/pkg/setting/setting_azure_test.go +++ b/pkg/setting/setting_azure_test.go @@ -63,4 +63,156 @@ func TestAzureSettings(t *testing.T) { }) } }) + + t.Run("User Identity", func(t *testing.T) { + t.Run("should be disabled by default", func(t *testing.T) { + cfg := NewCfg() + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure) + + assert.False(t, cfg.Azure.UserIdentityEnabled) + }) + + t.Run("should be enabled", func(t *testing.T) { + cfg := NewCfg() + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure) + require.NotNil(t, cfg.Azure.UserIdentityTokenEndpoint) + + assert.True(t, cfg.Azure.UserIdentityEnabled) + }) + + t.Run("should use token endpoint from Azure AD if enabled", func(t *testing.T) { + cfg := NewCfg() + + azureAdSection, err := cfg.Raw.NewSection("auth.azuread") + require.NoError(t, err) + _, err = azureAdSection.NewKey("enabled", "true") + require.NoError(t, err) + _, err = azureAdSection.NewKey("token_url", "URL_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_id", "ID_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_secret", "SECRET_1") + require.NoError(t, err) + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure) + require.NotNil(t, cfg.Azure.UserIdentityTokenEndpoint) + + assert.True(t, cfg.Azure.UserIdentityEnabled) + assert.Equal(t, "URL_1", cfg.Azure.UserIdentityTokenEndpoint.TokenUrl) + assert.Equal(t, "ID_1", cfg.Azure.UserIdentityTokenEndpoint.ClientId) + assert.Equal(t, "SECRET_1", cfg.Azure.UserIdentityTokenEndpoint.ClientSecret) + }) + + t.Run("should not use token endpoint from Azure AD if not enabled", func(t *testing.T) { + cfg := NewCfg() + + azureAdSection, err := cfg.Raw.NewSection("auth.azuread") + require.NoError(t, err) + _, err = azureAdSection.NewKey("enabled", "false") + require.NoError(t, err) + _, err = azureAdSection.NewKey("token_url", "URL_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_id", "ID_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_secret", "SECRET_1") + require.NoError(t, err) + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure) + require.NotNil(t, cfg.Azure.UserIdentityTokenEndpoint) + + assert.True(t, cfg.Azure.UserIdentityEnabled) + assert.Empty(t, cfg.Azure.UserIdentityTokenEndpoint.TokenUrl) + assert.Empty(t, cfg.Azure.UserIdentityTokenEndpoint.ClientId) + assert.Empty(t, cfg.Azure.UserIdentityTokenEndpoint.ClientSecret) + }) + + t.Run("should override Azure AD settings", func(t *testing.T) { + cfg := NewCfg() + + azureAdSection, err := cfg.Raw.NewSection("auth.azuread") + require.NoError(t, err) + _, err = azureAdSection.NewKey("enabled", "true") + require.NoError(t, err) + _, err = azureAdSection.NewKey("token_url", "URL_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_id", "ID_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_secret", "SECRET_1") + require.NoError(t, err) + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_token_url", "URL_2") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_client_id", "ID_2") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_client_secret", "SECRET_2") + require.NoError(t, err) + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure) + require.NotNil(t, cfg.Azure.UserIdentityTokenEndpoint) + + assert.True(t, cfg.Azure.UserIdentityEnabled) + assert.Equal(t, "URL_2", cfg.Azure.UserIdentityTokenEndpoint.TokenUrl) + assert.Equal(t, "ID_2", cfg.Azure.UserIdentityTokenEndpoint.ClientId) + assert.Equal(t, "SECRET_2", cfg.Azure.UserIdentityTokenEndpoint.ClientSecret) + }) + + t.Run("should not use secret from Azure AD if client ID overridden", func(t *testing.T) { + cfg := NewCfg() + + azureAdSection, err := cfg.Raw.NewSection("auth.azuread") + require.NoError(t, err) + _, err = azureAdSection.NewKey("enabled", "true") + require.NoError(t, err) + _, err = azureAdSection.NewKey("token_url", "URL_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_id", "ID_1") + require.NoError(t, err) + _, err = azureAdSection.NewKey("client_secret", "SECRET_1") + require.NoError(t, err) + + azureSection, err := cfg.Raw.NewSection("azure") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_enabled", "true") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_token_url", "URL_2") + require.NoError(t, err) + _, err = azureSection.NewKey("user_identity_client_id", "ID_2") + require.NoError(t, err) + + cfg.readAzureSettings() + require.NotNil(t, cfg.Azure) + require.NotNil(t, cfg.Azure.UserIdentityTokenEndpoint) + + assert.True(t, cfg.Azure.UserIdentityEnabled) + assert.Equal(t, "URL_2", cfg.Azure.UserIdentityTokenEndpoint.TokenUrl) + assert.Equal(t, "ID_2", cfg.Azure.UserIdentityTokenEndpoint.ClientId) + assert.Empty(t, cfg.Azure.UserIdentityTokenEndpoint.ClientSecret) + }) + }) }