Azure: Use SSO settings in plugin context (#112058)

* Bump grafana-azure-sdk-go

* Set override values

* Add Azure settings helper covering SSO cases

* Ensure Azure settings are correctly created

- Add mock for sso settings service
- Add tests
- Update wire

* Minor improvements

* Test updates

* Move fake implementation

* add interface to limit leakage

* rename

* work sync

* Fix wire

* Add fake provider

* Update tests

* Actually fix the workspace

* More go dependency fixes

* Update tests

* Update workspace, again

* Add missing tests

* Fix dependencies

* These dependencies..

* More dependency things

* Okay now dependencies really are fixed

* Lint

* Update pkg/services/pluginsintegration/pluginconfig/request.go

Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>

---------

Co-authored-by: Will Browne <wbrowne@tcd.ie>
Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
Andreas Christou
2025-10-10 11:46:26 +01:00
committed by GitHub
co-authored by Misi Will Browne
parent 59459560c2
commit fa9d6be255
15 changed files with 652 additions and 75 deletions
@@ -0,0 +1,66 @@
package pluginconfig
import (
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsso"
)
// mergeAzureSettings merges the Azure AD settings from the SSO settings DB with the Azure AD settings from the config.
// Azure AD settings can be changed via the UI or SSO settings API
// They can also be overridden in the [azure] config section
// The order of precedence is:
// 1. [azure] config section (if the override flag is set)
// 2. SSO settings from the DB (if they exist)
// 3. [auth.azuread] config section (if enabled)
func mergeAzureSettings(currSettings *azsettings.AzureSettings, azureAdSettings *pluginsso.Settings) *azsettings.AzureSettings {
if azureAdSettings != nil {
settings := azureAdSettings.Values
tokenEndpointSettings := currSettings.UserIdentityTokenEndpoint
if tokenEndpointSettings == nil {
tokenEndpointSettings = &azsettings.TokenEndpointSettings{}
currSettings.UserIdentityTokenEndpoint = tokenEndpointSettings
}
tokenUrl, ok := settings["token_url"].(string)
if ok {
if !currSettings.UserIdentityTokenEndpoint.TokenUrlOverride {
currSettings.UserIdentityTokenEndpoint.TokenUrl = tokenUrl
}
}
clientAuth, ok := settings["client_authentication"].(string)
if ok {
if !currSettings.UserIdentityTokenEndpoint.ClientAuthenticationOverride {
if clientAuth != "none" {
currSettings.UserIdentityTokenEndpoint.ClientAuthentication = clientAuth
}
}
}
clientId, ok := settings["client_id"].(string)
if ok {
if !currSettings.UserIdentityTokenEndpoint.ClientIdOverride {
currSettings.UserIdentityTokenEndpoint.ClientId = clientId
}
}
clientSecret, ok := settings["client_secret"].(string)
if ok {
if !currSettings.UserIdentityTokenEndpoint.ClientSecretOverride {
currSettings.UserIdentityTokenEndpoint.ClientSecret = clientSecret
}
}
managedIdentityClientId, ok := settings["managed_identity_client_id"].(string)
if ok {
if !currSettings.UserIdentityTokenEndpoint.ManagedIdentityClientIdOverride {
currSettings.UserIdentityTokenEndpoint.ManagedIdentityClientId = managedIdentityClientId
}
}
federatedCredentialAudience, ok := settings["federated_credential_audience"].(string)
if ok {
if !currSettings.UserIdentityTokenEndpoint.FederatedCredentialAudienceOverride {
currSettings.UserIdentityTokenEndpoint.FederatedCredentialAudience = federatedCredentialAudience
}
}
}
return currSettings
}
@@ -0,0 +1,197 @@
package pluginconfig
import (
"testing"
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsso"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetAzureSettings(t *testing.T) {
t.Run("no Azure settings input", func(t *testing.T) {
result := mergeAzureSettings(nil, nil)
assert.Nil(t, result)
})
t.Run("no SSO settings or override settings", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "original-token-url",
ClientAuthentication: "original-auth",
ClientId: "original-client-id",
ClientSecret: "original-client-secret",
},
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{},
}
result := mergeAzureSettings(currSettings, azureAdSettings)
assert.Equal(t, "original-token-url", result.UserIdentityTokenEndpoint.TokenUrl)
assert.Equal(t, "original-auth", result.UserIdentityTokenEndpoint.ClientAuthentication)
assert.Equal(t, "original-client-id", result.UserIdentityTokenEndpoint.ClientId)
assert.Equal(t, "original-client-secret", result.UserIdentityTokenEndpoint.ClientSecret)
})
t.Run("with SSO settings but no overrides", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "original-token-url",
ClientAuthentication: "original-auth",
ClientId: "original-client-id",
ClientSecret: "original-client-secret",
ManagedIdentityClientId: "original-managed-id",
FederatedCredentialAudience: "original-audience",
TokenUrlOverride: false,
ClientAuthenticationOverride: false,
ClientIdOverride: false,
ClientSecretOverride: false,
ManagedIdentityClientIdOverride: false,
FederatedCredentialAudienceOverride: false,
},
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{
"token_url": "sso-token-url",
"client_authentication": "sso-auth",
"client_id": "sso-client-id",
"client_secret": "sso-client-secret",
"managed_identity_client_id": "sso-managed-id",
"federated_credential_audience": "sso-audience",
},
}
result := mergeAzureSettings(currSettings, azureAdSettings)
assert.Equal(t, "sso-token-url", result.UserIdentityTokenEndpoint.TokenUrl)
assert.Equal(t, "sso-auth", result.UserIdentityTokenEndpoint.ClientAuthentication)
assert.Equal(t, "sso-client-id", result.UserIdentityTokenEndpoint.ClientId)
assert.Equal(t, "sso-client-secret", result.UserIdentityTokenEndpoint.ClientSecret)
assert.Equal(t, "sso-managed-id", result.UserIdentityTokenEndpoint.ManagedIdentityClientId)
assert.Equal(t, "sso-audience", result.UserIdentityTokenEndpoint.FederatedCredentialAudience)
})
t.Run("with both overrides and SSO settings", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "override-token-url",
ClientAuthentication: "override-auth",
ClientId: "override-client-id",
ClientSecret: "override-client-secret",
ManagedIdentityClientId: "override-managed-id",
FederatedCredentialAudience: "override-audience",
TokenUrlOverride: true,
ClientAuthenticationOverride: true,
ClientIdOverride: true,
ClientSecretOverride: true,
ManagedIdentityClientIdOverride: true,
FederatedCredentialAudienceOverride: true,
},
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{
"token_url": "sso-token-url",
"client_authentication": "sso-auth",
"client_id": "sso-client-id",
"client_secret": "sso-client-secret",
"managed_identity_client_id": "sso-managed-id",
"federated_credential_audience": "sso-audience",
},
}
result := mergeAzureSettings(currSettings, azureAdSettings)
// Should keep override values, not SSO values
assert.Equal(t, "override-token-url", result.UserIdentityTokenEndpoint.TokenUrl)
assert.Equal(t, "override-auth", result.UserIdentityTokenEndpoint.ClientAuthentication)
assert.Equal(t, "override-client-id", result.UserIdentityTokenEndpoint.ClientId)
assert.Equal(t, "override-client-secret", result.UserIdentityTokenEndpoint.ClientSecret)
assert.Equal(t, "override-managed-id", result.UserIdentityTokenEndpoint.ManagedIdentityClientId)
assert.Equal(t, "override-audience", result.UserIdentityTokenEndpoint.FederatedCredentialAudience)
})
t.Run("client authentication 'none' should be ignored", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
ClientAuthentication: "original-auth",
ClientAuthenticationOverride: false,
},
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{
"client_authentication": "none",
},
}
result := mergeAzureSettings(currSettings, azureAdSettings)
assert.Equal(t, "original-auth", result.UserIdentityTokenEndpoint.ClientAuthentication)
})
t.Run("non-string values should be ignored", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "original-token-url",
ClientId: "original-client-id",
TokenUrlOverride: false,
ClientIdOverride: false,
},
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{
"token_url": 12345,
"client_id": []string{"array", "value"},
},
}
result := mergeAzureSettings(currSettings, azureAdSettings)
assert.Equal(t, "original-token-url", result.UserIdentityTokenEndpoint.TokenUrl)
assert.Equal(t, "original-client-id", result.UserIdentityTokenEndpoint.ClientId)
})
t.Run("Nil UserIdentityTokenEndpoint should not panic", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: nil,
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{
"token_url": "sso-token-url",
},
}
require.NotPanics(t, func() {
mergeAzureSettings(currSettings, azureAdSettings)
})
})
t.Run("Empty SSO settings map", func(t *testing.T) {
currSettings := &azsettings.AzureSettings{
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "original-token-url",
ClientAuthentication: "original-auth",
ClientId: "original-client-id",
},
}
azureAdSettings := &pluginsso.Settings{
Values: map[string]any{},
}
result := mergeAzureSettings(currSettings, azureAdSettings)
assert.Equal(t, "original-token-url", result.UserIdentityTokenEndpoint.TokenUrl)
assert.Equal(t, "original-auth", result.UserIdentityTokenEndpoint.ClientAuthentication)
assert.Equal(t, "original-client-id", result.UserIdentityTokenEndpoint.ClientId)
})
}
@@ -13,23 +13,27 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/envvars"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsso"
)
var _ envvars.Provider = (*EnvVarsProvider)(nil)
type EnvVarsProvider struct {
cfg *PluginInstanceCfg
license plugins.Licensing
logger log.Logger
cfg *PluginInstanceCfg
license plugins.Licensing
logger log.Logger
ssoSettings pluginsso.SettingsProvider
}
func NewEnvVarsProvider(cfg *PluginInstanceCfg, license plugins.Licensing) *EnvVarsProvider {
func NewEnvVarsProvider(cfg *PluginInstanceCfg, license plugins.Licensing, ssoSettings pluginsso.SettingsProvider) *EnvVarsProvider {
return &EnvVarsProvider{
cfg: cfg,
license: license,
logger: log.New("plugins.envvars"),
cfg: cfg,
license: license,
logger: log.New("plugins.envvars"),
ssoSettings: ssoSettings,
}
}
@@ -63,7 +67,7 @@ func (p *EnvVarsProvider) PluginEnvVars(ctx context.Context, plugin *plugins.Plu
hostEnv = append(hostEnv, p.featureToggleEnableVars(ctx)...)
hostEnv = append(hostEnv, p.awsEnvVars(plugin.PluginID())...)
hostEnv = append(hostEnv, p.secureSocksProxyEnvVars()...)
hostEnv = append(hostEnv, azsettings.WriteToEnvStr(p.cfg.Azure)...)
hostEnv = append(hostEnv, azsettings.WriteToEnvStr(p.getAzureSettings())...)
hostEnv = append(hostEnv, p.tracingEnvVars(plugin)...)
hostEnv = append(hostEnv, p.pluginSettingsEnvVars(plugin.PluginID())...)
@@ -189,3 +193,15 @@ func (p *EnvVarsProvider) envVar(key, value string) string {
}
return fmt.Sprintf("%s=%s", key, value)
}
func (p *EnvVarsProvider) getAzureSettings() *azsettings.AzureSettings {
azureSettings := p.cfg.Azure
if azureSettings == nil {
azureSettings = &azsettings.AzureSettings{}
}
azureAdSettings, err := p.ssoSettings.GetForProvider(context.Background(), social.AzureADProviderName)
if err != nil {
p.logger.Error("Failed to get SSO settings", "error", err)
}
return mergeAzureSettings(azureSettings, azureAdSettings)
}
@@ -46,7 +46,7 @@ func TestPluginEnvVarsProvider_PluginEnvVars(t *testing.T) {
Features: featuremgmt.WithFeatures(),
}
provider := NewEnvVarsProvider(cfg, licensing)
provider := NewEnvVarsProvider(cfg, licensing, &fakeSSOSettingsProvider{})
envVars := provider.PluginEnvVars(context.Background(), p)
assert.Len(t, envVars, 6)
assert.Equal(t, "GF_VERSION=", envVars[0])
@@ -77,7 +77,7 @@ func TestPluginEnvVarsProvider_skipHostEnvVars(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
provider := NewEnvVarsProvider(pCfg, nil, &fakeSSOSettingsProvider{})
envVars := provider.PluginEnvVars(context.Background(), p)
// We want to test that the envvars.Provider does not add any of the host env vars.
@@ -93,7 +93,7 @@ func TestPluginEnvVarsProvider_skipHostEnvVars(t *testing.T) {
cfg := setting.NewCfg()
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
provider := NewEnvVarsProvider(pCfg, nil, &fakeSSOSettingsProvider{})
t.Run("should populate allowed host env vars", func(t *testing.T) {
// Set all allowed variables
@@ -418,7 +418,7 @@ func TestPluginEnvVarsProvider_tracingEnvironmentVariables(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
p := NewEnvVarsProvider(tc.cfg, nil)
p := NewEnvVarsProvider(tc.cfg, nil, &fakeSSOSettingsProvider{})
envVars := p.PluginEnvVars(context.Background(), tc.plugin)
tc.exp(t, envVars)
})
@@ -473,7 +473,7 @@ func TestPluginEnvVarsProvider_authEnvVars(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
provider := NewEnvVarsProvider(pCfg, nil, &fakeSSOSettingsProvider{})
envVars := provider.PluginEnvVars(context.Background(), p)
assert.Equal(t, "GF_VERSION=", envVars[0])
assert.Equal(t, "GF_APP_URL=https://myorg.com/", envVars[1])
@@ -521,7 +521,7 @@ func TestPluginEnvVarsProvider_awsEnvVars(t *testing.T) {
Features: featuremgmt.WithFeatures(),
}
provider := NewEnvVarsProvider(cfg, nil)
provider := NewEnvVarsProvider(cfg, nil, &fakeSSOSettingsProvider{})
envVars := provider.PluginEnvVars(context.Background(), p)
assert.ElementsMatch(t, tc.expected, envVars)
}
@@ -540,7 +540,7 @@ func TestPluginEnvVarsProvider_featureToggleEnvVar(t *testing.T) {
Features: featuremgmt.WithFeatures(expectedFeatures[0], true, expectedFeatures[1], true),
}
p := NewEnvVarsProvider(cfg, nil)
p := NewEnvVarsProvider(cfg, nil, &fakeSSOSettingsProvider{})
envVars := p.PluginEnvVars(context.Background(), &plugins.Plugin{})
assert.Equal(t, 2, len(envVars))
@@ -591,7 +591,7 @@ func TestPluginEnvVarsProvider_azureEnvVars(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil)
provider := NewEnvVarsProvider(pCfg, nil, &fakeSSOSettingsProvider{})
envVars := provider.PluginEnvVars(context.Background(), &plugins.Plugin{})
assert.ElementsMatch(t, []string{"GF_VERSION=", "GFAZPL_AZURE_CLOUD=AzureCloud", "GFAZPL_AZURE_AUTH_ENABLED=true",
"GFAZPL_MANAGED_IDENTITY_ENABLED=true",
@@ -608,4 +608,113 @@ func TestPluginEnvVarsProvider_azureEnvVars(t *testing.T) {
"GFAZPL_USER_IDENTITY_ASSERTION=username",
}, envVars)
})
t.Run("sets user token endpoint settings from SSO settings for an Azure plugin", func(t *testing.T) {
cfg := &setting.Cfg{
Raw: ini.Empty(),
Azure: &azsettings.AzureSettings{
AzureAuthEnabled: true,
Cloud: azsettings.AzurePublic,
ManagedIdentityEnabled: true,
ManagedIdentityClientId: "mock_managed_identity_client_id",
WorkloadIdentityEnabled: true,
WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{
TenantId: "mock_workload_identity_tenant_id",
ClientId: "mock_workload_identity_client_id",
TokenFile: "mock_workload_identity_token_file",
},
UserIdentityEnabled: true,
UserIdentityFallbackCredentialsEnabled: true,
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "mock_user_identity_token_url",
ClientId: "mock_user_identity_client_id",
ClientSecret: "mock_user_identity_client_secret",
UsernameAssertion: true,
},
},
}
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil, &fakeSSOSettingsProvider{
GetForProviderFunc: getAzureSSOSettings,
})
envVars := provider.PluginEnvVars(context.Background(), &plugins.Plugin{})
assert.ElementsMatch(t, []string{"GF_VERSION=", "GFAZPL_AZURE_CLOUD=AzureCloud", "GFAZPL_AZURE_AUTH_ENABLED=true",
"GFAZPL_MANAGED_IDENTITY_ENABLED=true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID=mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED=true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID=mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID=mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE=mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED=true",
"GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED=true",
"GFAZPL_USER_IDENTITY_TOKEN_URL=sso_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_AUTHENTICATION=sso_user_client_authentication",
"GFAZPL_USER_IDENTITY_CLIENT_ID=sso_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET=sso_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_MANAGED_IDENTITY_CLIENT_ID=sso_user_identity_managed_identity_client_id",
"GFAZPL_USER_IDENTITY_FEDERATED_CREDENTIAL_AUDIENCE=sso_user_identity_federated_credential_audience",
"GFAZPL_USER_IDENTITY_ASSERTION=username",
}, envVars)
})
t.Run("does not use SSO settings if overrides have been set for an Azure plugin", func(t *testing.T) {
cfg := &setting.Cfg{
Raw: ini.Empty(),
Azure: &azsettings.AzureSettings{
AzureAuthEnabled: true,
Cloud: azsettings.AzurePublic,
ManagedIdentityEnabled: true,
ManagedIdentityClientId: "mock_managed_identity_client_id",
WorkloadIdentityEnabled: true,
WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{
TenantId: "mock_workload_identity_tenant_id",
ClientId: "mock_workload_identity_client_id",
TokenFile: "mock_workload_identity_token_file",
},
UserIdentityEnabled: true,
UserIdentityFallbackCredentialsEnabled: true,
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "override_user_identity_token_url",
TokenUrlOverride: true,
ClientAuthentication: "override_user_client_authentication",
ClientAuthenticationOverride: true,
ClientId: "override_user_identity_client_id",
ClientIdOverride: true,
ClientSecret: "override_user_identity_client_secret",
ClientSecretOverride: true,
ManagedIdentityClientId: "override_user_identity_managed_identity_client_id",
ManagedIdentityClientIdOverride: true,
FederatedCredentialAudience: "override_user_identity_federated_credential_audience",
FederatedCredentialAudienceOverride: true,
},
},
}
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewEnvVarsProvider(pCfg, nil, &fakeSSOSettingsProvider{
GetForProviderFunc: getAzureSSOSettings,
})
envVars := provider.PluginEnvVars(context.Background(), &plugins.Plugin{})
assert.ElementsMatch(t, []string{"GF_VERSION=", "GFAZPL_AZURE_CLOUD=AzureCloud", "GFAZPL_AZURE_AUTH_ENABLED=true",
"GFAZPL_MANAGED_IDENTITY_ENABLED=true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID=mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED=true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID=mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID=mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE=mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED=true",
"GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED=true",
"GFAZPL_USER_IDENTITY_TOKEN_URL=override_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_AUTHENTICATION=override_user_client_authentication",
"GFAZPL_USER_IDENTITY_CLIENT_ID=override_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET=override_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_MANAGED_IDENTITY_CLIENT_ID=override_user_identity_managed_identity_client_id",
"GFAZPL_USER_IDENTITY_FEDERATED_CREDENTIAL_AUDIENCE=override_user_identity_federated_credential_audience",
}, envVars)
})
}
@@ -9,11 +9,14 @@ import (
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-azure-sdk-go/v2/azsettings"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/proxy"
"github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsso"
)
var _ PluginRequestConfigProvider = (*RequestConfigProvider)(nil)
@@ -23,12 +26,16 @@ type PluginRequestConfigProvider interface {
}
type RequestConfigProvider struct {
cfg *PluginInstanceCfg
cfg *PluginInstanceCfg
ssoSettings pluginsso.SettingsProvider
logger log.Logger
}
func NewRequestConfigProvider(cfg *PluginInstanceCfg) *RequestConfigProvider {
func NewRequestConfigProvider(cfg *PluginInstanceCfg, ssoSettings pluginsso.SettingsProvider) *RequestConfigProvider {
return &RequestConfigProvider{
cfg: cfg,
cfg: cfg,
ssoSettings: ssoSettings,
logger: log.New("pluginrequestconfig"),
}
}
@@ -89,8 +96,19 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI
if s.cfg.AzureAuthEnabled {
m[azsettings.AzureAuthEnabled] = strconv.FormatBool(s.cfg.AzureAuthEnabled)
}
azureSettings := s.cfg.Azure
if azureSettings != nil && slices.Contains[[]string, string](azureSettings.ForwardSettingsPlugins, pluginID) {
if azureSettings == nil {
azureSettings = &azsettings.AzureSettings{}
}
if slices.Contains(azureSettings.ForwardSettingsPlugins, pluginID) {
azureAdSettings, err := s.ssoSettings.GetForProvider(ctx, social.AzureADProviderName)
if err != nil {
s.logger.Error("Failed to get SSO settings", "error", err)
}
azureSettings = mergeAzureSettings(azureSettings, azureAdSettings)
if azureSettings.Cloud != "" {
m[azsettings.AzureCloud] = azureSettings.Cloud
}
@@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/plugins/auth"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsso"
"github.com/grafana/grafana/pkg/setting"
)
@@ -19,7 +20,7 @@ func TestRequestConfigProvider_PluginRequestConfig_Defaults(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Equal(t, map[string]string{
"GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0",
"GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0",
@@ -133,7 +134,7 @@ func TestRequestConfigProvider_PluginRequestConfig(t *testing.T) {
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
p := NewRequestConfigProvider(tc.cfg)
p := NewRequestConfigProvider(tc.cfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", nil), tc.expected)
})
}
@@ -168,7 +169,7 @@ func TestRequestConfigProvider_PluginRequestConfig_featureToggles(t *testing.T)
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), tc.features)
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", nil), tc.expectedConfig)
}
})
@@ -182,7 +183,7 @@ func TestRequestConfigProvider_PluginRequestConfig_appURL(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", nil), map[string]string{"GF_APP_URL": "https://myorg.com/"})
})
}
@@ -198,7 +199,7 @@ func TestRequestConfigProvider_PluginRequestConfig_SQL(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", nil), map[string]string{
"GF_SQL_ROW_LIMIT": "23",
"GF_SQL_MAX_OPEN_CONNS_DEFAULT": "24",
@@ -216,7 +217,7 @@ func TestRequestConfigProvider_PluginRequestConfig_SQL(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Equal(t, map[string]string{
"GF_SQL_MAX_OPEN_CONNS_DEFAULT": "0",
"GF_SQL_MAX_IDLE_CONNS_DEFAULT": "0",
@@ -233,7 +234,7 @@ func TestRequestConfigProvider_PluginRequestConfig_concurrentQueryCount(t *testi
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", nil), map[string]string{"GF_CONCURRENT_QUERY_COUNT": "42"})
})
@@ -242,7 +243,7 @@ func TestRequestConfigProvider_PluginRequestConfig_concurrentQueryCount(t *testi
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.NotContains(t, p.PluginRequestConfig(context.Background(), "", nil), "GF_CONCURRENT_QUERY_COUNT")
})
@@ -253,7 +254,7 @@ func TestRequestConfigProvider_PluginRequestConfig_concurrentQueryCount(t *testi
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.NotContains(t, p.PluginRequestConfig(context.Background(), "", nil), "GF_CONCURRENT_QUERY_COUNT")
})
}
@@ -265,7 +266,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azureAuthEnabled(t *testing.T
Features: featuremgmt.WithFeatures(),
}
p := NewRequestConfigProvider(cfg)
p := NewRequestConfigProvider(cfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", nil), map[string]string{"GFAZPL_AZURE_AUTH_ENABLED": "true"})
})
@@ -274,7 +275,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azureAuthEnabled(t *testing.T
Features: featuremgmt.WithFeatures(),
}
p := NewRequestConfigProvider(cfg)
p := NewRequestConfigProvider(cfg, &fakeSSOSettingsProvider{})
require.NotContains(t, p.PluginRequestConfig(context.Background(), "", nil), "GFAZPL_AZURE_AUTH_ENABLED")
})
@@ -284,7 +285,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azureAuthEnabled(t *testing.T
Features: featuremgmt.WithFeatures(),
}
p := NewRequestConfigProvider(cfg)
p := NewRequestConfigProvider(cfg, &fakeSSOSettingsProvider{})
require.NotContains(t, p.PluginRequestConfig(context.Background(), "", nil), "GFAZPL_AZURE_AUTH_ENABLED")
})
}
@@ -322,7 +323,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource", nil), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
@@ -356,7 +357,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource", nil), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
@@ -384,7 +385,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
m := p.PluginRequestConfig(context.Background(), "", nil)
require.NotContains(t, m, "GFAZPL_AZURE_CLOUD")
require.NotContains(t, m, "GFAZPL_MANAGED_IDENTITY_ENABLED")
@@ -413,7 +414,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource", nil), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
@@ -433,6 +434,108 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) {
"GFAZPL_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED": "true",
})
})
t.Run("sets user token endpoint settings from SSO settings for an Azure plugin", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.Azure = azSettings
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{
GetForProviderFunc: getAzureSSOSettings,
})
require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource", nil), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED": "true",
"GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED": "true",
"GFAZPL_USER_IDENTITY_TOKEN_URL": "sso_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_AUTHENTICATION": "sso_user_client_authentication",
"GFAZPL_USER_IDENTITY_CLIENT_ID": "sso_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET": "sso_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_MANAGED_IDENTITY_CLIENT_ID": "sso_user_identity_managed_identity_client_id",
"GFAZPL_USER_IDENTITY_FEDERATED_CREDENTIAL_AUDIENCE": "sso_user_identity_federated_credential_audience",
"GFAZPL_USER_IDENTITY_ASSERTION": "username",
"GFAZPL_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED": "true",
})
})
t.Run("does not use SSO settings if overrides have been set for an Azure plugin", func(t *testing.T) {
cfg := setting.NewCfg()
cfg.Azure = &azsettings.AzureSettings{
Cloud: azsettings.AzurePublic,
ManagedIdentityEnabled: true,
ManagedIdentityClientId: "mock_managed_identity_client_id",
WorkloadIdentityEnabled: true,
WorkloadIdentitySettings: &azsettings.WorkloadIdentitySettings{
TenantId: "mock_workload_identity_tenant_id",
ClientId: "mock_workload_identity_client_id",
TokenFile: "mock_workload_identity_token_file",
},
UserIdentityEnabled: true,
UserIdentityTokenEndpoint: &azsettings.TokenEndpointSettings{
TokenUrl: "override_user_identity_token_url",
TokenUrlOverride: true,
ClientAuthentication: "override_user_client_authentication",
ClientAuthenticationOverride: true,
ClientId: "override_user_identity_client_id",
ClientIdOverride: true,
ClientSecret: "override_user_identity_client_secret",
ClientSecretOverride: true,
ManagedIdentityClientId: "override_user_identity_managed_identity_client_id",
ManagedIdentityClientIdOverride: true,
FederatedCredentialAudience: "override_user_identity_federated_credential_audience",
FederatedCredentialAudienceOverride: true,
UsernameAssertion: true,
},
UserIdentityFallbackCredentialsEnabled: true,
ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql", "grafana-azureprometheus-datasource"},
AzureEntraPasswordCredentialsEnabled: true,
}
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{
GetForProviderFunc: getAzureSSOSettings,
})
require.Subset(t, p.PluginRequestConfig(context.Background(), "grafana-azure-monitor-datasource", nil), map[string]string{
"GFAZPL_AZURE_CLOUD": "AzureCloud", "GFAZPL_MANAGED_IDENTITY_ENABLED": "true",
"GFAZPL_MANAGED_IDENTITY_CLIENT_ID": "mock_managed_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_ENABLED": "true",
"GFAZPL_WORKLOAD_IDENTITY_TENANT_ID": "mock_workload_identity_tenant_id",
"GFAZPL_WORKLOAD_IDENTITY_CLIENT_ID": "mock_workload_identity_client_id",
"GFAZPL_WORKLOAD_IDENTITY_TOKEN_FILE": "mock_workload_identity_token_file",
"GFAZPL_USER_IDENTITY_ENABLED": "true",
"GFAZPL_USER_IDENTITY_FALLBACK_SERVICE_CREDENTIALS_ENABLED": "true",
"GFAZPL_USER_IDENTITY_TOKEN_URL": "override_user_identity_token_url",
"GFAZPL_USER_IDENTITY_CLIENT_AUTHENTICATION": "override_user_client_authentication",
"GFAZPL_USER_IDENTITY_CLIENT_ID": "override_user_identity_client_id",
"GFAZPL_USER_IDENTITY_CLIENT_SECRET": "override_user_identity_client_secret",
"GFAZPL_USER_IDENTITY_MANAGED_IDENTITY_CLIENT_ID": "override_user_identity_managed_identity_client_id",
"GFAZPL_USER_IDENTITY_FEDERATED_CREDENTIAL_AUDIENCE": "override_user_identity_federated_credential_audience",
"GFAZPL_USER_IDENTITY_ASSERTION": "username",
"GFAZPL_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED": "true",
})
})
}
func getAzureSSOSettings(ctx context.Context, provider string) (*pluginsso.Settings, error) {
return &pluginsso.Settings{
Values: map[string]any{
"token_url": "sso_user_identity_token_url",
"client_authentication": "sso_user_client_authentication",
"client_id": "sso_user_identity_client_id",
"client_secret": "sso_user_identity_client_secret",
"managed_identity_client_id": "sso_user_identity_managed_identity_client_id",
"federated_credential_audience": "sso_user_identity_federated_credential_audience",
},
}, nil
}
func TestRequestConfigProvider_PluginRequestConfig_aws(t *testing.T) {
@@ -447,7 +550,7 @@ func TestRequestConfigProvider_PluginRequestConfig_aws(t *testing.T) {
cfg.AWSListMetricsPageLimit = "100"
cfg.AWSForwardSettingsPlugins = []string{"cloudwatch", "prometheus", "elasticsearch"}
p := NewRequestConfigProvider(cfg)
p := NewRequestConfigProvider(cfg, &fakeSSOSettingsProvider{})
t.Run("uses the aws settings for an AWS plugin", func(t *testing.T) {
require.Subset(t, p.PluginRequestConfig(context.Background(), "cloudwatch", nil), map[string]string{
@@ -471,7 +574,7 @@ func TestRequestConfigProvider_PluginRequestConfig_aws(t *testing.T) {
t.Run("uses the aws settings for a non-aws user-specified plugin", func(t *testing.T) {
cfg.AWSForwardSettingsPlugins = append(cfg.AWSForwardSettingsPlugins, "test-datasource")
p = NewRequestConfigProvider(cfg)
p = NewRequestConfigProvider(cfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "test-datasource", nil), map[string]string{
"AWS_AUTH_AssumeRoleEnabled": "false",
"AWS_AUTH_AllowedAuthProviders": "grafana_assume_role,keys",
@@ -489,9 +592,20 @@ func TestRequestConfigProvider_PluginRequestConfig_appClientSecret(t *testing.T)
pCfg, err := ProvidePluginInstanceConfig(cfg, setting.ProvideProvider(cfg), featuremgmt.WithFeatures())
require.NoError(t, err)
p := NewRequestConfigProvider(pCfg)
p := NewRequestConfigProvider(pCfg, &fakeSSOSettingsProvider{})
require.Subset(t, p.PluginRequestConfig(context.Background(), "", &auth.ExternalService{
ClientSecret: "mysecret",
}), map[string]string{backend.AppClientSecret: "mysecret"})
})
}
type fakeSSOSettingsProvider struct {
GetForProviderFunc func(ctx context.Context, provider string) (*pluginsso.Settings, error)
}
func (m *fakeSSOSettingsProvider) GetForProvider(ctx context.Context, provider string) (*pluginsso.Settings, error) {
if m.GetForProviderFunc != nil {
return m.GetForProviderFunc(ctx, provider)
}
return nil, nil
}