From 319a8740337f28a6464887f3388ec61aa5945cab Mon Sep 17 00:00:00 2001 From: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:08:51 -0700 Subject: [PATCH] MSSQL: Password auth for Azure AD (#89746) * Password auth for Azure AD * rename auth fields * add azure flag for client password cred enabled * prettier * rename flag * Update go.mod * Update public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx Co-authored-by: Andreas Christou * Apply suggestions from code review Co-authored-by: Andreas Christou * update package * go mod * prettier * remove password * gowork * remove unused env test * linter --------- Co-authored-by: Andreas Christou --- conf/defaults.ini | 4 + conf/sample.ini | 4 + .../setup-grafana/configure-grafana/_index.md | 6 + go.mod | 2 +- go.work.sum | 2 + packages/grafana-runtime/src/config.ts | 2 + pkg/api/dtos/frontend_settings.go | 13 +- pkg/api/frontendsettings.go | 1 + .../pluginconfig/request.go | 2 + .../pluginconfig/request_test.go | 3 + pkg/setting/setting_azure.go | 2 + pkg/tsdb/mssql/mssql.go | 11 ++ .../mssql/azureauth/AzureAuth.testMocks.ts | 4 +- .../mssql/azureauth/AzureAuthSettings.tsx | 2 + .../mssql/azureauth/AzureCredentials.ts | 2 + .../mssql/azureauth/AzureCredentialsConfig.ts | 41 ++++- .../mssql/azureauth/AzureCredentialsForm.tsx | 150 ++++++++++++++---- .../configuration/ConfigurationEditor.tsx | 2 +- public/app/plugins/datasource/mssql/types.ts | 4 + 19 files changed, 215 insertions(+), 42 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index f81271210f5..6e3a19a5d0e 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -997,6 +997,10 @@ username_assertion = # By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL). forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql +# Specifies whether Entra password auth can be used for the MSSQL data source +# Disabled by default, needs to be explicitly enabled +azure_entra_password_credentials_enabled = false + #################################### 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 1515e157ff3..72db893cc20 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -984,6 +984,10 @@ # By default this will include all Grafana Labs owned Azure plugins, or those that make use of Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL). ;forward_settings_to_plugins = grafana-azure-monitor-datasource, prometheus, grafana-azure-data-explorer-datasource, mssql +# Specifies whether Entra password auth can be used for the MSSQL data source +# Disabled by default, needs to be explicitly enabled +;azure_entra_password_credentials_enabled = false + #################################### 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 8c2b089a81e..2959d0215e1 100644 --- a/docs/sources/setup-grafana/configure-grafana/_index.md +++ b/docs/sources/setup-grafana/configure-grafana/_index.md @@ -1275,6 +1275,12 @@ Set plugins that will receive Azure settings via plugin context. By default, this will include all Grafana Labs owned Azure plugins or those that use Azure settings (Azure Monitor, Azure Data Explorer, Prometheus, MSSQL). +### azure_entra_password_credentials_enabled + +Specifies whether Entra password auth can be used for the MSSQL data source. This authentication is not recommended and consideration should be taken before enabling this. + +Disabled by default, needs to be explicitly enabled. + ## [auth.jwt] Refer to [JWT authentication]({{< relref "../configure-security/configure-authentication/jwt" >}}) for more information. diff --git a/go.mod b/go.mod index 3ee335564ef..da11e274ed4 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,7 @@ require ( github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad github.com/grafana/gomemcache v0.0.0-20240229205252-cd6a66d6fb56 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-aws-sdk v0.28.0 // @grafana/aws-datasources - github.com/grafana/grafana-azure-sdk-go/v2 v2.0.4 // @grafana/partner-datasources + github.com/grafana/grafana-azure-sdk-go/v2 v2.1.0 // @grafana/partner-datasources github.com/grafana/grafana-cloud-migration-snapshot v1.1.0 // @grafana/grafana-operator-experience-squad github.com/grafana/grafana-google-sdk-go v0.1.0 // @grafana/partner-datasources github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 // @grafana/grafana-backend-group diff --git a/go.work.sum b/go.work.sum index 5c0b83e6359..ae6c759c6aa 100644 --- a/go.work.sum +++ b/go.work.sum @@ -416,6 +416,8 @@ github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b h1:Ha+kSIoTutf4ytlVw github.com/grafana/e2e v0.1.1-0.20221018202458-cffd2bb71c7b/go.mod h1:3UsooRp7yW5/NJQBlXcTsAHOoykEhNUYXkQ3r6ehEEY= github.com/grafana/e2e v0.1.1 h1:/b6xcv5BtoBnx8cZnCiey9DbjEc8z7gXHO5edoeRYxc= github.com/grafana/e2e v0.1.1/go.mod h1:RpNLgae5VT+BUHvPE+/zSypmOXKwEu4t+tnEMS1ATaE= +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/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b h1:HCbWyVL6vi7gxyO76gQksSPH203oBJ1MJ3JcG1OQlsg= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240422145632-c33c6b5b6e6b/go.mod h1:01sXtHoRwI8W324IPAzuxDFOmALqYLCOhvSC2fUHWXc= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 8d615b27d1a..305e128d186 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -26,6 +26,7 @@ export interface AzureSettings { workloadIdentityEnabled: boolean; userIdentityEnabled: boolean; userIdentityFallbackCredentialsEnabled: boolean; + azureEntraPasswordCredentialsEnabled: boolean; } export interface AzureCloudInfo { @@ -131,6 +132,7 @@ export class GrafanaBootConfig implements GrafanaConfig { workloadIdentityEnabled: false, userIdentityEnabled: false, userIdentityFallbackCredentialsEnabled: false, + azureEntraPasswordCredentialsEnabled: false, }; caching = { enabled: false, diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index a58b0ca00b1..9d233cbb55b 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -66,12 +66,13 @@ type FrontendSettingsLicenseInfoDTO struct { } type FrontendSettingsAzureDTO struct { - Cloud string `json:"cloud"` - Clouds []azsettings.AzureCloudInfo `json:"clouds"` - ManagedIdentityEnabled bool `json:"managedIdentityEnabled"` - WorkloadIdentityEnabled bool `json:"workloadIdentityEnabled"` - UserIdentityEnabled bool `json:"userIdentityEnabled"` - UserIdentityFallbackCredentialsEnabled bool `json:"userIdentityFallbackCredentialsEnabled"` + Cloud string `json:"cloud,omitempty"` + Clouds []azsettings.AzureCloudInfo `json:"clouds,omitempty"` + ManagedIdentityEnabled bool `json:"managedIdentityEnabled,omitempty"` + WorkloadIdentityEnabled bool `json:"workloadIdentityEnabled,omitempty"` + UserIdentityEnabled bool `json:"userIdentityEnabled,omitempty"` + UserIdentityFallbackCredentialsEnabled bool `json:"userIdentityFallbackCredentialsEnabled,omitempty"` + AzureEntraPasswordCredentialsEnabled bool `json:"azureEntraPasswordCredentialsEnabled,omitempty"` } type FrontendSettingsCachingDTO struct { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index abb5a3783da..6eb4ae8ee6a 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -276,6 +276,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro WorkloadIdentityEnabled: hs.Cfg.Azure.WorkloadIdentityEnabled, UserIdentityEnabled: hs.Cfg.Azure.UserIdentityEnabled, UserIdentityFallbackCredentialsEnabled: hs.Cfg.Azure.UserIdentityFallbackCredentialsEnabled, + AzureEntraPasswordCredentialsEnabled: hs.Cfg.Azure.AzureEntraPasswordCredentialsEnabled, }, Caching: dtos.FrontendSettingsCachingDTO{ diff --git a/pkg/services/pluginsintegration/pluginconfig/request.go b/pkg/services/pluginsintegration/pluginconfig/request.go index c42e32c663d..347855a8946 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request.go +++ b/pkg/services/pluginsintegration/pluginconfig/request.go @@ -142,6 +142,8 @@ func (s *RequestConfigProvider) PluginRequestConfig(ctx context.Context, pluginI } } } + + m[azsettings.AzureEntraPasswordCredentialsEnabled] = strconv.FormatBool(azureSettings.AzureEntraPasswordCredentialsEnabled) } if s.cfg.UserFacingDefaultError != "" { diff --git a/pkg/services/pluginsintegration/pluginconfig/request_test.go b/pkg/services/pluginsintegration/pluginconfig/request_test.go index a6d57386457..c5eaa7c467c 100644 --- a/pkg/services/pluginsintegration/pluginconfig/request_test.go +++ b/pkg/services/pluginsintegration/pluginconfig/request_test.go @@ -309,6 +309,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { }, UserIdentityFallbackCredentialsEnabled: true, ForwardSettingsPlugins: []string{"grafana-azure-monitor-datasource", "prometheus", "grafana-azure-data-explorer-datasource", "mssql"}, + AzureEntraPasswordCredentialsEnabled: true, } t.Run("uses the azure settings for an Azure plugin", func(t *testing.T) { @@ -389,6 +390,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_ID") require.NotContains(t, m, "GFAZPL_USER_IDENTITY_CLIENT_SECRET") require.NotContains(t, m, "GFAZPL_USER_IDENTITY_ASSERTION") + require.NotContains(t, m, "GFAZPL_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED") }) t.Run("uses the azure settings for a non-Azure user-specified plugin", func(t *testing.T) { @@ -413,6 +415,7 @@ func TestRequestConfigProvider_PluginRequestConfig_azure(t *testing.T) { "GFAZPL_USER_IDENTITY_CLIENT_ID": "mock_user_identity_client_id", "GFAZPL_USER_IDENTITY_CLIENT_SECRET": "mock_user_identity_client_secret", "GFAZPL_USER_IDENTITY_ASSERTION": "username", + "GFAZPL_AZURE_ENTRA_PASSWORD_CREDENTIALS_ENABLED": "true", }) }) } diff --git a/pkg/setting/setting_azure.go b/pkg/setting/setting_azure.go index 13ab1a575c6..c2531ca4270 100644 --- a/pkg/setting/setting_azure.go +++ b/pkg/setting/setting_azure.go @@ -80,5 +80,7 @@ func (cfg *Cfg) readAzureSettings() { azureSettings.ForwardSettingsPlugins = util.SplitString(azureSection.Key("forward_settings_to_plugins").String()) + azureSettings.AzureEntraPasswordCredentialsEnabled = azureSection.Key("azure_entra_password_credentials_enabled").MustBool(false) + cfg.Azure = azureSettings } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index c73b9925b68..230c4b5de1a 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -321,6 +321,17 @@ func getAzureCredentialDSNFragment(azureCredentials azcredentials.AzureCredentia c.ClientSecret, "ActiveDirectoryApplication", ) + case *azcredentials.AzureEntraPasswordCredentials: + if cfg.Azure.AzureEntraPasswordCredentialsEnabled { + connStr += fmt.Sprintf("user id=%s;password=%s;applicationclientid=%s;fedauth=%s;", + c.UserId, + c.Password, + c.ClientId, + "ActiveDirectoryPassword", + ) + } else { + return "", fmt.Errorf("azure entra password authentication is not enabled") + } default: return "", fmt.Errorf("unsupported azure authentication type") } diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts index 42daca490ef..8c4fda9035e 100644 --- a/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts +++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts @@ -9,6 +9,7 @@ export const configWithManagedIdentityEnabled: Partial = { workloadIdentityEnabled: false, userIdentityEnabled: false, userIdentityFallbackCredentialsEnabled: false, + azureEntraPasswordCredentialsEnabled: false, }, }; @@ -19,6 +20,7 @@ export const configWithManagedIdentityDisabled: Partial = { userIdentityEnabled: false, cloud: 'AzureCloud', userIdentityFallbackCredentialsEnabled: false, + azureEntraPasswordCredentialsEnabled: false, }, }; @@ -48,5 +50,5 @@ export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial< DataSourceSettings > = { ...basicJSONData, - secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' }, + secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX', password: undefined }, }; diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx index 9e2271e720c..2213abff501 100644 --- a/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx +++ b/public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx @@ -13,6 +13,7 @@ import { AzureCredentialsForm } from './AzureCredentialsForm'; export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { const { dataSourceConfig: dsSettings, onChange } = props; const managedIdentityEnabled = config.azure.managedIdentityEnabled; + const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled; const credentials = useMemo(() => getCredentials(dsSettings, config), [dsSettings]); @@ -30,6 +31,7 @@ export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { return ( { const concealedSecret: ConcealedSecretType = Symbol('Concealed client secret'); - if (clientSecretStoredServerSide) { + if (storedServerSide) { // The secret is concealed server side, so return the symbol return concealedSecret; } else { - return typeof clientSecret === 'string' && clientSecret.length > 0 ? clientSecret : undefined; + return typeof secret === 'string' && secret.length > 0 ? secret : undefined; } }; @@ -41,6 +41,8 @@ export const getCredentials = ( // Secure JSON data/fields const clientSecretStoredServerSide = dsSettings.secureJsonFields?.azureClientSecret; const clientSecret = dsSettings.secureJsonData?.azureClientSecret; + const passwordStoredServerSide = dsSettings.secureJsonFields?.password; + const password = dsSettings.secureJsonData?.password; // BootConfig data const managedIdentityEnabled = !!bootConfig.azure?.managedIdentityEnabled; @@ -74,6 +76,13 @@ export const getCredentials = ( clientId: credentials.clientId, clientSecret: getSecret(clientSecretStoredServerSide, clientSecret), }; + case AzureAuthType.AD_PASSWORD: + return { + authType: AzureAuthType.AD_PASSWORD, + userId: credentials.userId, + clientId: credentials.clientId, + password: getSecret(passwordStoredServerSide, password), + }; } }; @@ -130,5 +139,29 @@ export const updateCredentials = ( }; return dsSettings; + + case AzureAuthType.AD_PASSWORD: + return { + ...dsSettings, + jsonData: { + ...dsSettings.jsonData, + azureCredentials: { + authType: AzureAuthType.AD_PASSWORD, + userId: credentials.userId, + clientId: credentials.clientId, + }, + }, + secureJsonData: { + ...dsSettings.secureJsonData, + password: + typeof credentials.password === 'string' && credentials.password.length > 0 + ? credentials.password + : undefined, + }, + secureJsonFields: { + ...dsSettings.secureJsonFields, + password: typeof credentials.password === 'symbol', + }, + }; } }; diff --git a/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx b/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx index 86d28a6c825..442f75bb7fa 100644 --- a/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx @@ -7,25 +7,22 @@ import { AzureCredentialsType, AzureAuthType } from '../types'; export interface Props { managedIdentityEnabled: boolean; + azureEntraPasswordCredentialsEnabled: boolean; credentials: AzureCredentialsType; azureCloudOptions?: SelectableValue[]; onCredentialsChange: (updatedCredentials: AzureCredentialsType) => void; disabled?: boolean; } -const authTypeOptions: Array> = [ - { - value: AzureAuthType.MSI, - label: 'Managed Identity', - }, - { - value: AzureAuthType.CLIENT_SECRET, - label: 'App Registration', - }, -]; - export const AzureCredentialsForm = (props: Props) => { - const { managedIdentityEnabled, credentials, azureCloudOptions, onCredentialsChange, disabled } = props; + const { + managedIdentityEnabled, + azureEntraPasswordCredentialsEnabled, + credentials, + azureCloudOptions, + onCredentialsChange, + disabled, + } = props; const onAuthTypeChange = (selected: SelectableValue) => { if (onCredentialsChange) { @@ -37,8 +34,27 @@ export const AzureCredentialsForm = (props: Props) => { } }; + const authTypeOptions: Array> = [ + { + value: AzureAuthType.CLIENT_SECRET, + label: 'App Registration', + }, + ]; + if (managedIdentityEnabled) { + authTypeOptions.push({ + value: AzureAuthType.MSI, + label: 'Managed Identity', + }); + } + if (azureEntraPasswordCredentialsEnabled) { + authTypeOptions.push({ + value: AzureAuthType.AD_PASSWORD, + label: 'Azure Entra Password', + }); + } + const onInputChange = ({ property, value }: { property: keyof AzureCredentialsType; value: string }) => { - if (onCredentialsChange && credentials.authType === 'clientsecret') { + if (onCredentialsChange) { const updated: AzureCredentialsType = { ...credentials, [property]: value, @@ -49,22 +65,20 @@ export const AzureCredentialsForm = (props: Props) => { return (
- {managedIdentityEnabled && ( - - opt.value === credentials.authType)} + options={authTypeOptions} + onChange={onAuthTypeChange} + disabled={disabled} + /> + + {credentials.authType === AzureAuthType.CLIENT_SECRET && ( <> {azureCloudOptions && ( @@ -167,6 +181,84 @@ export const AzureCredentialsForm = (props: Props) => { ))} )} + {credentials.authType === AzureAuthType.AD_PASSWORD && azureEntraPasswordCredentialsEnabled && ( + <> + + ) => { + const value = event.target.value; + onInputChange({ property: 'userId', value }); + }} + disabled={disabled} + aria-label="User ID" + /> + + + ) => { + const value = event.target.value; + onInputChange({ property: 'clientId', value }); + }} + disabled={disabled} + aria-label="Application Client ID" + /> + + {!disabled && + (typeof credentials.password === 'symbol' ? ( + +
+ + +
+
+ ) : ( + + ) => { + const value = event.target.value; + onInputChange({ property: 'password', value }); + }} + id="password" + disabled={disabled} + /> + + ))} + + )}
); }; diff --git a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx index 1fc0152f8e7..54ff2af3ae5 100644 --- a/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx +++ b/public/app/plugins/datasource/mssql/configuration/ConfigurationEditor.tsx @@ -108,7 +108,7 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps