[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>
This commit is contained in:
co-authored by
Adam Yeats
parent
0e6bcba4a8
commit
cc30b2fbb1
@@ -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
|
||||
|
||||
|
@@ -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))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+17
-8
@@ -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>
|
||||
|
||||
-1
@@ -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' },
|
||||
|
||||
-1
@@ -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;
|
||||
|
||||
+61
-5
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user