diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index ef8bc6a3396..4a8bfa27949 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -77,6 +77,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `pinNavItems` | Enables pinning of nav items | Yes | | `openSearchBackendFlowEnabled` | Enables the backend query flow for Open Search datasource plugin | Yes | | `cloudWatchRoundUpEndTime` | Round up end time for metric queries to the next minute to avoid missing data | Yes | +| `azureMonitorEnableUserAuth` | Enables user auth for Azure Monitor datasource only | Yes | ## Public preview feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b8124c22120..a479281af0e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -221,4 +221,5 @@ export interface FeatureToggles { rolePickerDrawer?: boolean; unifiedStorageSearch?: boolean; pluginsSriChecks?: boolean; + azureMonitorEnableUserAuth?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ba68086a0af..5d9c0ff960d 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1524,6 +1524,13 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "azureMonitorEnableUserAuth", + Description: "Enables user auth for Azure Monitor datasource only", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaPartnerPluginsSquad, + Expression: "true", // Enabled by default for now + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 27dc4bcdf2a..f0e13459d1a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -202,3 +202,4 @@ useSessionStorageForRedirection,preview,@grafana/identity-access-team,false,fals rolePickerDrawer,experimental,@grafana/identity-access-team,false,false,false unifiedStorageSearch,experimental,@grafana/search-and-storage,false,false,false pluginsSriChecks,experimental,@grafana/plugins-platform-backend,false,false,false +azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 147d0f4681e..5af82d7231d 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -818,4 +818,8 @@ const ( // FlagPluginsSriChecks // Enables SRI checks for plugin assets FlagPluginsSriChecks = "pluginsSriChecks" + + // FlagAzureMonitorEnableUserAuth + // Enables user auth for Azure Monitor datasource only + FlagAzureMonitorEnableUserAuth = "azureMonitorEnableUserAuth" ) diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index fbd7b6e335a..cdf2ffdec1e 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -590,6 +590,19 @@ "codeowner": "@grafana/aws-datasources" } }, + { + "metadata": { + "name": "azureMonitorEnableUserAuth", + "resourceVersion": "1733500361181", + "creationTimestamp": "2024-12-06T15:52:41Z" + }, + "spec": { + "description": "Enables user auth for Azure Monitor datasource only", + "stage": "GA", + "codeowner": "@grafana/partner-datasources", + "expression": "true" + } + }, { "metadata": { "name": "azureMonitorPrometheusExemplars", diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index b5bf3dcd5a0..25dbc2abb6d 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor.go +++ b/pkg/tsdb/azuremonitor/azuremonitor.go @@ -10,6 +10,7 @@ import ( "net/http" "strconv" + "github.com/grafana/grafana-azure-sdk-go/v2/azcredentials" "github.com/grafana/grafana-azure-sdk-go/v2/azsettings" "github.com/grafana/grafana-azure-sdk-go/v2/azusercontext" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -116,6 +117,10 @@ func NewInstanceSettings(clientProvider *httpclient.Provider, executors map[stri return nil, err } + if credentials.AzureAuthType() == azcredentials.AzureAuthCurrentUserIdentity && !backend.GrafanaConfigFromContext(ctx).FeatureToggles().IsEnabled("azureMonitorEnableUserAuth") { + return nil, backend.DownstreamError(errors.New("current user authentication is not enabled for azure monitor")) + } + model := types.DatasourceInfo{ Credentials: credentials, Settings: azMonitorSettings, diff --git a/pkg/tsdb/azuremonitor/azuremonitor_test.go b/pkg/tsdb/azuremonitor/azuremonitor_test.go index 6cde8cb6792..cfbe5385f29 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/log" + "github.com/grafana/grafana-plugin-sdk-go/experimental/featuretoggles" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" @@ -58,9 +59,29 @@ func TestNewInstanceSettings(t *testing.T) { tests := []struct { name string settings backend.DataSourceInstanceSettings - expectedModel types.DatasourceInfo + expectedModel *types.DatasourceInfo Err require.ErrorAssertionFunc + setupContext func(ctx context.Context) context.Context }{ + { + name: "current user authentication disabled by feature toggle", + settings: backend.DataSourceInstanceSettings{ + JSONData: []byte(`{"azureAuthType":"currentuser"}`), + DecryptedSecureJSONData: map[string]string{}, + ID: 60, + }, + expectedModel: nil, + Err: func(t require.TestingT, err error, _ ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "current user authentication is not enabled for azure monitor") + }, + setupContext: func(ctx context.Context) context.Context { + featureToggles := backend.NewGrafanaCfg(map[string]string{ + featuretoggles.EnabledFeatures: "", // No enabled features + }) + return backend.WithGrafanaConfig(ctx, featureToggles) + }, + }, { name: "creates an instance", settings: backend.DataSourceInstanceSettings{ @@ -68,7 +89,7 @@ func TestNewInstanceSettings(t *testing.T) { DecryptedSecureJSONData: map[string]string{"key": "value"}, ID: 40, }, - expectedModel: types.DatasourceInfo{ + expectedModel: &types.DatasourceInfo{ Credentials: &azcredentials.AzureManagedIdentityCredentials{}, Settings: types.AzureMonitorSettings{}, Routes: testRoutes, @@ -86,7 +107,7 @@ func TestNewInstanceSettings(t *testing.T) { DecryptedSecureJSONData: map[string]string{"clientSecret": "secret"}, ID: 50, }, - expectedModel: types.DatasourceInfo{ + expectedModel: &types.DatasourceInfo{ Credentials: &azcredentials.AzureClientSecretCredentials{ AzureCloud: "AzureCustomizedCloud", ClientSecret: "secret", @@ -116,11 +137,23 @@ func TestNewInstanceSettings(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if tt.setupContext != nil { + ctx = tt.setupContext(ctx) + } + factory := NewInstanceSettings(&httpclient.Provider{}, map[string]azDatasourceExecutor{}, log.DefaultLogger) - instance, err := factory(context.Background(), tt.settings) + instance, err := factory(ctx, tt.settings) + tt.Err(t, err) - if !cmp.Equal(instance, tt.expectedModel) { - t.Errorf("Unexpected instance: %v", cmp.Diff(instance, tt.expectedModel)) + + if tt.expectedModel == nil { + require.Nil(t, instance, "Expected instance to be nil") + } else { + require.NotNil(t, instance, "Expected instance to be created") + if !cmp.Equal(instance, *tt.expectedModel) { + t.Errorf("Unexpected instance: %v", cmp.Diff(instance, *tt.expectedModel)) + } } }) } diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx index 92a7aa76431..499f166f5cd 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx @@ -74,17 +74,27 @@ export const AzureCredentialsForm = (props: Props) => { }, [managedIdentityEnabled, workloadIdentityEnabled, userIdentityEnabled]); const onAuthTypeChange = (selected: SelectableValue) => { - const defaultAuthType = managedIdentityEnabled - ? 'msi' - : workloadIdentityEnabled - ? 'workloadidentity' - : userIdentityEnabled - ? 'currentuser' - : 'clientsecret'; + const defaultAuthType = (() => { + if (managedIdentityEnabled) { + return 'msi'; + } + + if (workloadIdentityEnabled) { + return 'workloadidentity'; + } + + if (userIdentityEnabled) { + return 'currentuser'; + } + + return 'clientsecret'; + })(); + const updated: AzureCredentials = { ...credentials, authType: selected.value || defaultAuthType, }; + onCredentialsChange(updated); }; @@ -123,7 +133,6 @@ export const AzureCredentialsForm = (props: Props) => { disabled={disabled} managedIdentityEnabled={managedIdentityEnabled} workloadIdentityEnabled={workloadIdentityEnabled} - userIdentityEnabled={userIdentityEnabled} /> )} diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.test.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.test.tsx index 07ca09121f8..b466a0d27ac 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.test.tsx @@ -10,7 +10,6 @@ const setup = (propsFunc?: (props: Props) => Props) => { let props: Props = { managedIdentityEnabled: true, workloadIdentityEnabled: true, - userIdentityEnabled: true, credentials: { authType: 'currentuser' }, azureCloudOptions: [ { value: 'AzureCloud', label: 'Azure' }, diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx index 9802c32752b..e6f1537c155 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx @@ -14,7 +14,6 @@ import { AppRegistrationCredentials } from './AppRegistrationCredentials'; export interface Props { managedIdentityEnabled: boolean; workloadIdentityEnabled: boolean; - userIdentityEnabled: boolean; credentials: AadCurrentUserCredentials; azureCloudOptions?: SelectableValue[]; onCredentialsChange: (updatedCredentials: AzureCredentials) => void; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.test.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.test.tsx index 992bab732f6..67601dc52f4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.test.tsx @@ -1,21 +1,38 @@ -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, fireEvent, cleanup } from '@testing-library/react'; + +import { config } from '@grafana/runtime'; import { createMockDatasourceSettings } from '../../__mocks__/datasourceSettings'; +import { AzureCloud } from '../../types'; import { MonitorConfig, Props } from './MonitorConfig'; -const mockDatasourceSettings = createMockDatasourceSettings(); - const defaultProps: Props = { - options: mockDatasourceSettings, + options: createMockDatasourceSettings(), updateOptions: jest.fn(), getSubscriptions: jest.fn().mockResolvedValue([]), }; describe('MonitorConfig', () => { + beforeEach(() => { + config.azure = { + ...config.azure, + managedIdentityEnabled: false, + workloadIdentityEnabled: false, + userIdentityEnabled: false, + }; + config.featureToggles = { + azureMonitorEnableUserAuth: false, + }; + }); + + afterEach(() => { + cleanup(); + jest.clearAllMocks(); + }); + it('should render component', () => { render(); - expect(screen.getByText('Azure Cloud')).toBeInTheDocument(); }); @@ -29,6 +46,7 @@ describe('MonitorConfig', () => { expect(defaultProps.updateOptions).toHaveBeenCalled(); expect(screen.getByText('Azure Cloud')).toBeInTheDocument(); }); + expect(defaultProps.options.jsonData.azureAuthType).toBe('clientsecret'); it('should render component and set the default subscription if specified', async () => { @@ -39,4 +57,42 @@ describe('MonitorConfig', () => { expect(screen.getByText('Azure Cloud')).toBeInTheDocument(); await waitFor(() => expect(screen.getByText('Test Sub')).toBeInTheDocument()); }); + + it('should render with user identity enabled when feature toggle is true', async () => { + config.azure.userIdentityEnabled = true; + config.featureToggles.azureMonitorEnableUserAuth = true; + config.azure.cloud = AzureCloud.Public; + + const optionsWithUserAuth = createMockDatasourceSettings({ + jsonData: { azureAuthType: 'currentuser' }, + }); + + render(); + + const authDropdownInput = screen.getByTestId('data-testid auth-type').querySelector('input'); + + if (authDropdownInput) { + fireEvent.mouseDown(authDropdownInput); + } + + await waitFor(() => { + expect( + screen.getByText( + (content, element) => element?.tagName?.toLowerCase() === 'span' && /Current User/i.test(content) + ) + ).toBeInTheDocument(); + }); + }); + + it('should render with user identity disabled when feature toggle is false', async () => { + config.azure.userIdentityEnabled = true; + config.featureToggles.azureMonitorEnableUserAuth = false; + + render(); + + await waitFor(() => { + expect(screen.getByText('Authentication')).toBeInTheDocument(); + expect(screen.queryByText(/Current User/i)).not.toBeInTheDocument(); + }); + }); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx index 5be22c99907..dedabc45def 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx @@ -52,7 +52,7 @@ export const MonitorConfig = (props: Props) => {