[v11.3.x] Azure Monitor: Add a feature flag to toggle user auth for Azure Monitor only (#97576)

* Azure Monitor: Add a feature flag to toggle user auth for Azure Monitor only (#96858)

* Azure Monitor: Add a feature flag to toggle user auth for Azure Monitor only

* Fix condition for userIdentityEnabled

* Re-add removed test

* Remove unused prop

* Refactor onAuthTypeChange in AzureCredentialsForm

* Add frontend unit tests

* Lint

(cherry picked from commit b898a4540d)

# Conflicts:
#	docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
#	packages/grafana-data/src/types/featureToggles.gen.ts
#	pkg/services/featuremgmt/registry.go
#	pkg/services/featuremgmt/toggles_gen.csv
#	pkg/services/featuremgmt/toggles_gen.go
#	pkg/services/featuremgmt/toggles_gen.json

* Update test

* Fix lint

---------

Co-authored-by: Adam Yeats <16296989+adamyeats@users.noreply.github.com>
pull/97750/head
Andreas Christou 7 months ago committed by GitHub
parent 0e6bcba4a8
commit cc30b2fbb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 7
      pkg/services/featuremgmt/registry.go
  4. 1
      pkg/services/featuremgmt/toggles_gen.csv
  5. 4
      pkg/services/featuremgmt/toggles_gen.go
  6. 13
      pkg/services/featuremgmt/toggles_gen.json
  7. 5
      pkg/tsdb/azuremonitor/azuremonitor.go
  8. 45
      pkg/tsdb/azuremonitor/azuremonitor_test.go
  9. 25
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx
  10. 1
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.test.tsx
  11. 1
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx
  12. 66
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.test.tsx
  13. 2
      public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx

@ -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

@ -221,4 +221,5 @@ export interface FeatureToggles {
rolePickerDrawer?: boolean;
unifiedStorageSearch?: boolean;
pluginsSriChecks?: boolean;
azureMonitorEnableUserAuth?: boolean;
}

@ -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
},
}
)

@ -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

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
202 rolePickerDrawer experimental @grafana/identity-access-team false false false
203 unifiedStorageSearch experimental @grafana/search-and-storage false false false
204 pluginsSriChecks experimental @grafana/plugins-platform-backend false false false
205 azureMonitorEnableUserAuth GA @grafana/partner-datasources false false false

@ -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"
)

@ -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",

@ -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,

@ -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))
}
}
})
}

@ -74,17 +74,27 @@ export const AzureCredentialsForm = (props: Props) => {
}, [managedIdentityEnabled, workloadIdentityEnabled, userIdentityEnabled]);
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
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}
/>
)}
</ConfigSection>

@ -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' },

@ -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;

@ -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(<MonitorConfig {...defaultProps} />);
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(<MonitorConfig {...defaultProps} options={optionsWithUserAuth} />);
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(<MonitorConfig {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('Authentication')).toBeInTheDocument();
expect(screen.queryByText(/Current User/i)).not.toBeInTheDocument();
});
});
});

@ -52,7 +52,7 @@ export const MonitorConfig = (props: Props) => {
<AzureCredentialsForm
managedIdentityEnabled={config.azure.managedIdentityEnabled}
workloadIdentityEnabled={config.azure.workloadIdentityEnabled}
userIdentityEnabled={config.azure.userIdentityEnabled}
userIdentityEnabled={config.azure.userIdentityEnabled && !!config.featureToggles.azureMonitorEnableUserAuth}
credentials={credentials}
azureCloudOptions={getAzureCloudOptions()}
onCredentialsChange={onCredentialsChange}

Loading…
Cancel
Save