Datasources: Simplify the AzureCredentials structure in datasource config (#39209)

Related #35857

Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
Sergey Kostrukov 2022-01-25 05:23:32 -08:00 committed by GitHub
parent 58b8d84085
commit cb09162cde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 68 additions and 92 deletions

View File

@ -129,6 +129,9 @@ export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = (props) => {
/> />
); );
const azureAuthEnabled: boolean =
(azureAuthSettings?.azureAuthSupported && azureAuthSettings.getAzureAuthEnabled(dataSourceConfig)) || false;
return ( return (
<div className="gf-form-group"> <div className="gf-form-group">
<> <>
@ -219,16 +222,16 @@ export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = (props) => {
/> />
</div> </div>
{azureAuthSettings?.azureAuthEnabled && ( {azureAuthSettings?.azureAuthSupported && (
<div className="gf-form-inline"> <div className="gf-form-inline">
<Switch <Switch
label="Azure Authentication" label="Azure Authentication"
labelClass="width-13" labelClass="width-13"
checked={dataSourceConfig.jsonData.azureAuth || false} checked={azureAuthEnabled}
onChange={(event) => { onChange={(event) => {
onSettingsChange({ onSettingsChange(
jsonData: { ...dataSourceConfig.jsonData, azureAuth: event!.currentTarget.checked }, azureAuthSettings.setAzureAuthEnabled(dataSourceConfig, event!.currentTarget.checked)
}); );
}} }}
tooltip="Use Azure authentication for Azure endpoint." tooltip="Use Azure authentication for Azure endpoint."
/> />
@ -267,11 +270,9 @@ export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = (props) => {
</> </>
)} )}
{azureAuthSettings?.azureAuthEnabled && {azureAuthSettings?.azureAuthSupported && azureAuthEnabled && azureAuthSettings.azureSettingsUI && (
azureAuthSettings?.azureSettingsUI && <azureAuthSettings.azureSettingsUI dataSourceConfig={dataSourceConfig} onChange={onChange} />
dataSourceConfig.jsonData.azureAuth && ( )}
<azureAuthSettings.azureSettingsUI dataSourceConfig={dataSourceConfig} onChange={onChange} />
)}
{dataSourceConfig.jsonData.sigV4Auth && sigV4AuthToggleEnabled && <SigV4AuthSettings {...props} />} {dataSourceConfig.jsonData.sigV4Auth && sigV4AuthToggleEnabled && <SigV4AuthSettings {...props} />}

View File

@ -2,8 +2,20 @@ import React from 'react';
import { DataSourceSettings } from '@grafana/data'; import { DataSourceSettings } from '@grafana/data';
export interface AzureAuthSettings { export interface AzureAuthSettings {
azureAuthEnabled: boolean; /** Set to true if Azure authentication supported by the datasource */
azureSettingsUI?: React.ComponentType<HttpSettingsBaseProps>; readonly azureAuthSupported: boolean;
/** Gets whether the Azure authentication currently enabled for the datasource */
readonly getAzureAuthEnabled: (config: DataSourceSettings<any, any>) => boolean;
/** Enables/disables the Azure authentication from the datasource */
readonly setAzureAuthEnabled: (
config: DataSourceSettings<any, any>,
enabled: boolean
) => Partial<DataSourceSettings<any, any>>;
/** Optional React component of additional Azure settings UI if authentication is enabled */
readonly azureSettingsUI?: React.ComponentType<HttpSettingsBaseProps>;
} }
export interface HttpSettingsBaseProps<JSONData = any, SecureJSONData = any> { export interface HttpSettingsBaseProps<JSONData = any, SecureJSONData = any> {

View File

@ -16,17 +16,11 @@ const azureMiddlewareName = "AzureAuthentication.Provider"
func AzureMiddleware(cfg *setting.Cfg) httpclient.Middleware { func AzureMiddleware(cfg *setting.Cfg) httpclient.Middleware {
return httpclient.NamedMiddlewareFunc(azureMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper { return httpclient.NamedMiddlewareFunc(azureMiddlewareName, func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper {
if enabled, err := isAzureAuthenticationEnabled(opts.CustomOptions); err != nil {
return errorResponse(err)
} else if !enabled {
return next
}
credentials, err := getAzureCredentials(opts.CustomOptions) credentials, err := getAzureCredentials(opts.CustomOptions)
if err != nil { if err != nil {
return errorResponse(err) return errorResponse(err)
} else if credentials == nil { } else if credentials == nil {
credentials = getDefaultAzureCredentials(cfg) return next
} }
tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, credentials) tokenProvider, err := aztokenprovider.NewAzureAccessTokenProvider(cfg, credentials)
@ -49,17 +43,6 @@ func errorResponse(err error) http.RoundTripper {
}) })
} }
func isAzureAuthenticationEnabled(customOptions map[string]interface{}) (bool, error) {
if untypedValue, ok := customOptions["_azureAuth"]; !ok {
return false, nil
} else if value, ok := untypedValue.(bool); !ok {
err := fmt.Errorf("the field 'azureAuth' should be a bool")
return false, err
} else {
return value, nil
}
}
func getAzureCredentials(customOptions map[string]interface{}) (azcredentials.AzureCredentials, error) { func getAzureCredentials(customOptions map[string]interface{}) (azcredentials.AzureCredentials, error) {
if untypedValue, ok := customOptions["_azureCredentials"]; !ok { if untypedValue, ok := customOptions["_azureCredentials"]; !ok {
return nil, nil return nil, nil
@ -71,16 +54,6 @@ func getAzureCredentials(customOptions map[string]interface{}) (azcredentials.Az
} }
} }
func getDefaultAzureCredentials(cfg *setting.Cfg) azcredentials.AzureCredentials {
if cfg.Azure.ManagedIdentityEnabled {
return &azcredentials.AzureManagedIdentityCredentials{}
} else {
return &azcredentials.AzureClientSecretCredentials{
AzureCloud: cfg.Azure.Cloud,
}
}
}
func getAzureEndpointResourceId(customOptions map[string]interface{}) (*url.URL, error) { func getAzureEndpointResourceId(customOptions map[string]interface{}) (*url.URL, error) {
var value string var value string
if untypedValue, ok := customOptions["azureEndpointResourceId"]; !ok { if untypedValue, ok := customOptions["azureEndpointResourceId"]; !ok {

View File

@ -277,14 +277,13 @@ func (s *Service) httpClientOptions(ds *models.DataSource) (*sdkhttpclient.Optio
} }
} }
if ds.JsonData != nil && ds.JsonData.Get("azureAuth").MustBool() { if ds.JsonData != nil {
credentials, err := azcredentials.FromDatasourceData(ds.JsonData.MustMap(), s.DecryptedValues(ds)) credentials, err := azcredentials.FromDatasourceData(ds.JsonData.MustMap(), s.DecryptedValues(ds))
if err != nil { if err != nil {
err = fmt.Errorf("invalid Azure credentials: %s", err) err = fmt.Errorf("invalid Azure credentials: %s", err)
return nil, err return nil, err
} }
opts.CustomOptions["_azureAuth"] = true
if credentials != nil { if credentials != nil {
opts.CustomOptions["_azureCredentials"] = credentials opts.CustomOptions["_azureCredentials"] = credentials
} }

View File

@ -570,7 +570,7 @@ func TestService_HTTPClientOptions(t *testing.T) {
} }
t.Run("Azure authentication", func(t *testing.T) { t.Run("Azure authentication", func(t *testing.T) {
t.Run("should be disabled if not enabled in JsonData", func(t *testing.T) { t.Run("should be disabled if no Azure credentials configured", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData }) t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore()) secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
@ -579,32 +579,13 @@ func TestService_HTTPClientOptions(t *testing.T) {
opts, err := dsService.httpClientOptions(&ds) opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err) require.NoError(t, err)
assert.NotEqual(t, true, opts.CustomOptions["_azureAuth"])
assert.NotContains(t, opts.CustomOptions, "_azureCredentials") assert.NotContains(t, opts.CustomOptions, "_azureCredentials")
}) })
t.Run("should be enabled if enabled in JsonData without credentials configured", func(t *testing.T) { t.Run("should be enabled if Azure credentials configured", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData }) t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{ ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureAuth": true,
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
assert.Equal(t, true, opts.CustomOptions["_azureAuth"])
assert.NotContains(t, opts.CustomOptions, "_azureCredentials")
})
t.Run("should be enabled if enabled in JsonData with credentials configured", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureAuth": true,
"azureCredentials": map[string]interface{}{ "azureCredentials": map[string]interface{}{
"authType": "msi", "authType": "msi",
}, },
@ -616,39 +597,16 @@ func TestService_HTTPClientOptions(t *testing.T) {
opts, err := dsService.httpClientOptions(&ds) opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, true, opts.CustomOptions["_azureAuth"])
require.Contains(t, opts.CustomOptions, "_azureCredentials") require.Contains(t, opts.CustomOptions, "_azureCredentials")
credentials := opts.CustomOptions["_azureCredentials"] credentials := opts.CustomOptions["_azureCredentials"]
assert.IsType(t, &azcredentials.AzureManagedIdentityCredentials{}, credentials) assert.IsType(t, &azcredentials.AzureManagedIdentityCredentials{}, credentials)
}) })
t.Run("should be disabled if disabled in JsonData even with credentials configured", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureAuth": false,
"azureCredentials": map[string]interface{}{
"authType": "msi",
},
})
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := ProvideService(bus.New(), nil, secretsService, &acmock.Mock{})
opts, err := dsService.httpClientOptions(&ds)
require.NoError(t, err)
assert.NotEqual(t, true, opts.CustomOptions["_azureAuth"])
assert.NotContains(t, opts.CustomOptions, "_azureCredentials")
})
t.Run("should fail if credentials are invalid", func(t *testing.T) { t.Run("should fail if credentials are invalid", func(t *testing.T) {
t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData }) t.Cleanup(func() { ds.JsonData = emptyJsonData; ds.SecureJsonData = emptySecureJsonData })
ds.JsonData = simplejson.NewFromAny(map[string]interface{}{ ds.JsonData = simplejson.NewFromAny(map[string]interface{}{
"azureAuth": true,
"azureCredentials": "invalid", "azureCredentials": "invalid",
}) })

View File

@ -18,16 +18,25 @@ function getSecret(options: DataSourceSettings<any, any>): undefined | string |
} }
} }
export function hasCredentials(options: DataSourceSettings<any, any>): boolean {
return !!options.jsonData.azureCredentials;
}
export function getDefaultCredentials(): AzureCredentials {
if (config.azure.managedIdentityEnabled) {
return { authType: 'msi' };
} else {
return { authType: 'clientsecret', azureCloud: getDefaultAzureCloud() };
}
}
export function getCredentials(options: DataSourceSettings<any, any>): AzureCredentials { export function getCredentials(options: DataSourceSettings<any, any>): AzureCredentials {
const credentials = options.jsonData.azureCredentials as AzureCredentials | undefined; const credentials = options.jsonData.azureCredentials as AzureCredentials | undefined;
// If no credentials saved, then return empty credentials // If no credentials saved, then return empty credentials
// of type based on whether the managed identity enabled // of type based on whether the managed identity enabled
if (!credentials) { if (!credentials) {
return { return getDefaultCredentials();
authType: config.azure.managedIdentityEnabled ? 'msi' : 'clientsecret',
azureCloud: getDefaultAzureCloud(),
};
} }
switch (credentials.authType) { switch (credentials.authType) {
@ -105,3 +114,23 @@ export function updateCredentials(
return options; return options;
} }
} }
export function setDefaultCredentials(options: DataSourceSettings<any, any>): Partial<DataSourceSettings<any, any>> {
return {
jsonData: {
...options.jsonData,
azureCredentials: getDefaultCredentials(),
},
};
}
export function resetCredentials(options: DataSourceSettings<any, any>): Partial<DataSourceSettings<any, any>> {
return {
jsonData: {
...options.jsonData,
azureAuth: undefined,
azureCredentials: undefined,
azureEndpointResourceId: undefined,
},
};
}

View File

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { AlertingSettings, DataSourceHttpSettings, Alert } from '@grafana/ui'; import { AlertingSettings, DataSourceHttpSettings, Alert } from '@grafana/ui';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; import { DataSourcePluginOptionsEditorProps, DataSourceSettings } from '@grafana/data';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { PromOptions } from '../types'; import { PromOptions } from '../types';
import { AzureAuthSettings } from './AzureAuthSettings'; import { AzureAuthSettings } from './AzureAuthSettings';
import { PromSettings } from './PromSettings'; import { PromSettings } from './PromSettings';
import { hasCredentials, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
import { getAllAlertmanagerDataSources } from 'app/features/alerting/unified/utils/alertmanager'; import { getAllAlertmanagerDataSources } from 'app/features/alerting/unified/utils/alertmanager';
export type Props = DataSourcePluginOptionsEditorProps<PromOptions>; export type Props = DataSourcePluginOptionsEditorProps<PromOptions>;
@ -13,7 +14,10 @@ export const ConfigEditor = (props: Props) => {
const alertmanagers = getAllAlertmanagerDataSources(); const alertmanagers = getAllAlertmanagerDataSources();
const azureAuthSettings = { const azureAuthSettings = {
azureAuthEnabled: config.featureToggles['prometheus_azure_auth'] ?? false, azureAuthSupported: config.featureToggles['prometheus_azure_auth'] ?? false,
getAzureAuthEnabled: (config: DataSourceSettings<any, any>): boolean => hasCredentials(config),
setAzureAuthEnabled: (config: DataSourceSettings<any, any>, enabled: boolean) =>
enabled ? setDefaultCredentials(config) : resetCredentials(config),
azureSettingsUI: AzureAuthSettings, azureSettingsUI: AzureAuthSettings,
}; };