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 ab8dad22ef3..3f7c27beb59 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -81,6 +81,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `azureMonitorDisableLogLimit` | Disables the log limit restriction for Azure Monitor when true. The limit is enabled by default. | | | `preinstallAutoUpdate` | Enables automatic updates for pre-installed plugins | Yes | | `alertingUIOptimizeReducer` | Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query | 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 9c23ab2d47a..a4add656705 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -241,5 +241,6 @@ export interface FeatureToggles { jaegerBackendMigration?: boolean; reportingUseRawTimeRange?: boolean; alertingUIOptimizeReducer?: boolean; + azureMonitorEnableUserAuth?: boolean; alertingNotificationsStepMode?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2c9b654252c..d0d09f33e72 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1666,6 +1666,13 @@ var ( Owner: grafanaAlertingSquad, Expression: "true", // enabled by default }, + { + Name: "azureMonitorEnableUserAuth", + Description: "Enables user auth for Azure Monitor datasource only", + Stage: FeatureStageGeneralAvailability, + Owner: grafanaPartnerPluginsSquad, + Expression: "true", // Enabled by default for now + }, { Name: "alertingNotificationsStepMode", Description: "Enables simplified step mode in the notifications section", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 2896b4d9eea..1db211b319a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -222,4 +222,5 @@ crashDetection,experimental,@grafana/observability-traces-and-profiling,false,fa jaegerBackendMigration,experimental,@grafana/oss-big-tent,false,false,false reportingUseRawTimeRange,preview,@grafana/sharing-squad,false,false,false alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true +azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false alertingNotificationsStepMode,experimental,@grafana/alerting-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 588d9c5ff60..ab64c99ac64 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -899,6 +899,10 @@ const ( // Enables removing the reducer from the alerting UI when creating a new alert rule and using instant query FlagAlertingUIOptimizeReducer = "alertingUIOptimizeReducer" + // FlagAzureMonitorEnableUserAuth + // Enables user auth for Azure Monitor datasource only + FlagAzureMonitorEnableUserAuth = "azureMonitorEnableUserAuth" + // FlagAlertingNotificationsStepMode // Enables simplified step mode in the notifications section FlagAlertingNotificationsStepMode = "alertingNotificationsStepMode" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e8ecedb79fb..4b1cc4124d7 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -660,6 +660,22 @@ "expression": "false" } }, + { + "metadata": { + "name": "azureMonitorEnableUserAuth", + "resourceVersion": "1732189410576", + "creationTimestamp": "2024-11-21T11:42:29Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-11-21 11:43:30.576196 +0000 UTC" + } + }, + "spec": { + "description": "Enables user auth for Azure Monitor datasource only", + "stage": "GA", + "codeowner": "@grafana/partner-datasources", + "expression": "true" + } + }, { "metadata": { "name": "azureMonitorLogLimit", diff --git a/pkg/tsdb/azuremonitor/azuremonitor.go b/pkg/tsdb/azuremonitor/azuremonitor.go index 442ef4d2cea..476386c90a2 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" @@ -139,6 +140,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 992c5b76db2..ae5cb7f6630 100644 --- a/pkg/tsdb/azuremonitor/azuremonitor_test.go +++ b/pkg/tsdb/azuremonitor/azuremonitor_test.go @@ -17,6 +17,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" @@ -59,9 +60,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{ @@ -69,7 +90,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, @@ -87,7 +108,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", @@ -117,11 +138,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 3857dba771c..fa80caadc82 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx @@ -73,17 +73,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); }; @@ -122,7 +132,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 9d5fbf3063f..951afa88344 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx @@ -13,7 +13,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..e7d2331d954 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,37 @@ -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 { 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 +45,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 +56,41 @@ 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; + + 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 94ea592bf3d..a989a01056d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx @@ -53,7 +53,7 @@ export const MonitorConfig = (props: Props) => {