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
This commit is contained in:
Sergey Kostrukov 2023-05-15 10:00:54 -07:00 committed by GitHub
parent 5ec0f82baa
commit eafba8fa69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 250 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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,

View File

@ -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 {

View File

@ -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{

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
})
})
}