From 44d206c272dbe0c4113a17a950c7440b0e67088b Mon Sep 17 00:00:00 2001 From: Younjin Song Date: Tue, 12 Nov 2024 05:36:48 -0500 Subject: [PATCH] Azure: Unify credentials in frontend (#95354) * init * fix lint * fix lint * lint * update version * fix --- .../azuremonitor/__mocks__/datasource.ts | 5 +- .../__mocks__/datasourceSettings.ts | 6 +- .../__mocks__/instanceSettings.ts | 4 +- .../azure_log_analytics_datasource.ts | 22 +- .../azure_monitor_datasource.test.ts | 9 +- .../azure_monitor/azure_monitor_datasource.ts | 25 +- .../azure_resource_graph_datasource.ts | 4 +- .../AppRegistrationCredentials.tsx | 2 +- .../ConfigEditor/AzureCredentialsForm.tsx | 3 +- .../ConfigEditor/BasicLogsToggle.tsx | 4 +- .../components/ConfigEditor/ConfigEditor.tsx | 17 +- .../CurrentUserFallbackCredentials.tsx | 8 +- .../ConfigEditor/DefaultSubscription.tsx | 6 +- .../components/ConfigEditor/MonitorConfig.tsx | 9 +- .../components/QueryEditor/QueryEditor.tsx | 4 +- .../datasource/azuremonitor/credentials.ts | 313 ++++-------------- .../datasource/azuremonitor/datasource.ts | 8 +- .../plugins/datasource/azuremonitor/module.ts | 4 +- .../resourcePicker/resourcePickerData.ts | 11 +- .../datasource/azuremonitor/types/types.ts | 77 +---- 20 files changed, 155 insertions(+), 386 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts index 311bd737f8b..f4f4dcaf0d5 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/datasource.ts @@ -1,14 +1,13 @@ -import { DataSourceInstanceSettings } from '@grafana/data'; import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import Datasource from '../datasource'; -import { AzureDataSourceJsonData } from '../types'; +import { AzureMonitorDataSourceInstanceSettings } from '../types'; import { createMockInstanceSetttings } from './instanceSettings'; import { DeepPartial } from './utils'; export interface Context { - instanceSettings: DataSourceInstanceSettings; + instanceSettings: AzureMonitorDataSourceInstanceSettings; templateSrv: TemplateSrv; datasource: Datasource; getResource: jest.Mock; diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/datasourceSettings.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/datasourceSettings.ts index e6d5cf49c35..ec558625438 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/datasourceSettings.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/datasourceSettings.ts @@ -1,13 +1,13 @@ import { KeyValue } from '@grafana/data'; -import { AzureDataSourceSettings } from '../types'; +import { AzureMonitorDataSourceSettings } from '../types'; import { DeepPartial } from './utils'; export const createMockDatasourceSettings = ( - overrides?: DeepPartial, + overrides?: DeepPartial, secureJsonFieldsOverrides?: KeyValue -): AzureDataSourceSettings => { +): AzureMonitorDataSourceSettings => { return { id: 1, uid: 'uid', diff --git a/public/app/plugins/datasource/azuremonitor/__mocks__/instanceSettings.ts b/public/app/plugins/datasource/azuremonitor/__mocks__/instanceSettings.ts index 7a378285151..9c984c870a9 100644 --- a/public/app/plugins/datasource/azuremonitor/__mocks__/instanceSettings.ts +++ b/public/app/plugins/datasource/azuremonitor/__mocks__/instanceSettings.ts @@ -1,12 +1,12 @@ import { DataSourceInstanceSettings, PluginType } from '@grafana/data'; -import { AzureDataSourceInstanceSettings } from '../types'; +import { AzureMonitorDataSourceInstanceSettings } from '../types'; import { DeepPartial, mapPartialArrayObject } from './utils'; export const createMockInstanceSetttings = ( overrides?: DeepPartial -): AzureDataSourceInstanceSettings => { +): AzureMonitorDataSourceInstanceSettings => { const metaOverrides = overrides?.meta; return { url: '/ds/1', diff --git a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts index 872778fd9b8..f8dfad2b8e5 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts @@ -1,13 +1,15 @@ import { map } from 'lodash'; -import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; +import { AzureCredentials } from '@grafana/azure-sdk'; +import { ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import ResponseParser from '../azure_monitor/response_parser'; -import { getAuthType } from '../credentials'; +import { getCredentials } from '../credentials'; import { AzureAPIResponse, - AzureDataSourceJsonData, + AzureMonitorDataSourceInstanceSettings, + AzureMonitorDataSourceJsonData, AzureLogsVariable, AzureMonitorQuery, AzureQueryType, @@ -21,8 +23,9 @@ import { transformMetadataToKustoSchema } from './utils'; export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< AzureMonitorQuery, - AzureDataSourceJsonData + AzureMonitorDataSourceJsonData > { + readonly credentials: AzureCredentials; resourcePath: string; declare applicationId: string; @@ -32,10 +35,11 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< firstWorkspace?: string; constructor( - private instanceSettings: DataSourceInstanceSettings, + private instanceSettings: AzureMonitorDataSourceInstanceSettings, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); + this.credentials = getCredentials(instanceSettings); this.resourcePath = `${routeNames.logAnalytics}`; this.azureMonitorPath = `${routeNames.azureMonitor}/subscriptions`; @@ -222,17 +226,15 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend< } private validateDatasource(): DatasourceValidationResult | undefined { - const authType = getAuthType(this.instanceSettings); - - if (authType === 'clientsecret') { - if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) { + if (this.credentials.authType === 'clientsecret') { + if (!this.isValidConfigField(this.credentials.tenantId)) { return { status: 'error', message: 'The Tenant Id field is required.', }; } - if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) { + if (!this.isValidConfigField(this.credentials.clientId)) { return { status: 'error', message: 'The Client Id field is required.', diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts index fda13f2d851..9962ffdedd0 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.test.ts @@ -1,12 +1,10 @@ import { get, set } from 'lodash'; -import { DataSourceInstanceSettings } from '@grafana/data'; - import createMockQuery from '../__mocks__/query'; import { createTemplateVariables } from '../__mocks__/utils'; import { multiVariable } from '../__mocks__/variables'; import AzureMonitorDatasource from '../datasource'; -import { AzureAPIResponse, AzureDataSourceJsonData, Location } from '../types'; +import { AzureAPIResponse, AzureMonitorDataSourceInstanceSettings, Location } from '../types'; let replace = () => ''; @@ -24,7 +22,7 @@ jest.mock('@grafana/runtime', () => { }); interface TestContext { - instanceSettings: DataSourceInstanceSettings; + instanceSettings: AzureMonitorDataSourceInstanceSettings; ds: AzureMonitorDatasource; } @@ -37,7 +35,7 @@ describe('AzureMonitorDatasource', () => { name: 'test', url: 'http://azuremonitor.com', jsonData: { subscriptionId: 'mock-subscription-id', cloudName: 'azuremonitor' }, - } as unknown as DataSourceInstanceSettings; + } as unknown as AzureMonitorDataSourceInstanceSettings; ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings); }); @@ -664,6 +662,7 @@ describe('AzureMonitorDatasource', () => { beforeEach(() => { ctx.instanceSettings.jsonData.azureAuthType = 'msi'; + ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings); ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response); }); diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts index 3a89c892930..bd610440838 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts @@ -1,13 +1,15 @@ import { Namespace } from 'i18next'; import { find, startsWith } from 'lodash'; -import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; +import { AzureCredentials } from '@grafana/azure-sdk'; +import { ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; -import { getAuthType } from '../credentials'; +import { getCredentials } from '../credentials'; import TimegrainConverter from '../time_grain_converter'; import { - AzureDataSourceJsonData, + AzureMonitorDataSourceInstanceSettings, + AzureMonitorDataSourceJsonData, AzureMonitorMetricsMetadataResponse, AzureMonitorQuery, AzureQueryType, @@ -37,7 +39,11 @@ function hasValue(item?: string) { return !!(item && item !== defaultDropdownValue); } -export default class AzureMonitorDatasource extends DataSourceWithBackend { +export default class AzureMonitorDatasource extends DataSourceWithBackend< + AzureMonitorQuery, + AzureMonitorDataSourceJsonData +> { + private readonly credentials: AzureCredentials; apiVersion = '2018-01-01'; apiPreviewVersion = '2017-12-01-preview'; listByResourceGroupApiVersion = '2021-04-01'; @@ -50,10 +56,11 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend, + instanceSettings: AzureMonitorDataSourceInstanceSettings, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); + this.credentials = getCredentials(instanceSettings); this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId; this.basicLogsEnabled = instanceSettings.jsonData.basicLogsEnabled; @@ -306,17 +313,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend { filterQuery(item: AzureMonitorQuery): boolean { return !!item.azureResourceGraph?.query && !!item.subscriptions && item.subscriptions.length > 0; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx index d953ea5829f..1bdce1de9a0 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AppRegistrationCredentials.tsx @@ -1,10 +1,10 @@ import { ChangeEvent } from 'react'; +import { AzureClientSecretCredentials, AzureCredentials } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; import { Field, Select, Input, Button } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureClientSecretCredentials, AzureCredentials } from '../../types'; export interface AppRegistrationCredentialsProps { credentials: AzureClientSecretCredentials; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx index 92a7aa76431..3857dba771c 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/AzureCredentialsForm.tsx @@ -1,12 +1,11 @@ import { useMemo } from 'react'; -import { getAzureClouds } from '@grafana/azure-sdk'; +import { AzureAuthType, AzureCredentials, getAzureClouds } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; import { ConfigSection } from '@grafana/experimental'; import { Select, Field } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; -import { AzureAuthType, AzureCredentials } from '../../types'; import { AppRegistrationCredentials } from './AppRegistrationCredentials'; import CurrentUserFallbackCredentials from './CurrentUserFallbackCredentials'; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx index 4c674701b6e..79ad74c1e3e 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/BasicLogsToggle.tsx @@ -3,10 +3,10 @@ import * as React from 'react'; import { Field, Switch, useTheme2 } from '@grafana/ui'; -import { AzureDataSourceJsonData } from '../../types'; +import { AzureMonitorDataSourceJsonData } from '../../types'; export interface Props { - options: AzureDataSourceJsonData; + options: AzureMonitorDataSourceJsonData; onBasicLogsEnabledChange: (basicLogsEnabled: boolean) => void; } diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx index bf1d2b1c90d..e4246af0996 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/ConfigEditor.tsx @@ -8,16 +8,19 @@ import { Alert, Divider, SecureSocksProxySettings } from '@grafana/ui'; import ResponseParser from '../../azure_monitor/response_parser'; import { AzureAPIResponse, - AzureDataSourceJsonData, - AzureDataSourceSecureJsonData, - AzureDataSourceSettings, + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceSecureJsonData, + AzureMonitorDataSourceSettings, Subscription, } from '../../types'; import { routeNames } from '../../utils/common'; import { MonitorConfig } from './MonitorConfig'; -export type Props = DataSourcePluginOptionsEditorProps; +export type Props = DataSourcePluginOptionsEditorProps< + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceSecureJsonData +>; interface ErrorMessage { title: string; @@ -43,7 +46,9 @@ export class ConfigEditor extends PureComponent { this.baseURL = `/api/datasources/${this.props.options.id}/resources/${routeNames.azureMonitor}/subscriptions`; } - private updateOptions = (optionsFunc: (options: AzureDataSourceSettings) => AzureDataSourceSettings): void => { + private updateOptions = ( + optionsFunc: (options: AzureMonitorDataSourceSettings) => AzureMonitorDataSourceSettings + ): void => { const updated = optionsFunc(this.props.options); this.props.onOptionsChange(updated); @@ -54,7 +59,7 @@ export class ConfigEditor extends PureComponent { if (this.state.unsaved) { await getBackendSrv() .put(`/api/datasources/${this.props.options.id}`, this.props.options) - .then((result: { datasource: AzureDataSourceSettings }) => { + .then((result: { datasource: AzureMonitorDataSourceSettings }) => { updateDatasourcePluginOption(this.props, 'version', result.datasource.version); }); diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx index 9802c32752b..9d5fbf3063f 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/CurrentUserFallbackCredentials.tsx @@ -1,13 +1,12 @@ import { useMemo } from 'react'; +import { AadCurrentUserCredentials, AzureCredentials, instanceOfAzureCredential } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; import { ConfigSection } from '@grafana/experimental'; import { config } from '@grafana/runtime'; import { Select, Field, RadioButtonGroup, Alert, Stack } from '@grafana/ui'; -import { instanceOfAzureCredential } from '../../credentials'; import { selectors } from '../../e2e/selectors'; -import { AadCurrentUserCredentials, AzureAuthType, AzureCredentials } from '../../types'; import { AppRegistrationCredentials } from './AppRegistrationCredentials'; @@ -32,8 +31,9 @@ export const CurrentUserFallbackCredentials = (props: Props) => { workloadIdentityEnabled, } = props; + type FallbackCredentialAuthTypeOptions = 'clientsecret' | 'msi' | 'workloadidentity'; const authTypeOptions = useMemo(() => { - let opts: Array>> = [ + let opts: Array> = [ { value: 'clientsecret', label: 'App Registration', @@ -57,7 +57,7 @@ export const CurrentUserFallbackCredentials = (props: Props) => { return opts; }, [managedIdentityEnabled, workloadIdentityEnabled]); - const onAuthTypeChange = (selected: SelectableValue>) => { + const onAuthTypeChange = (selected: SelectableValue) => { const defaultAuthType = managedIdentityEnabled ? 'msi' : workloadIdentityEnabled diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx index bc8e7c79649..dc2af88ab78 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/DefaultSubscription.tsx @@ -1,14 +1,14 @@ import { useEffect, useReducer } from 'react'; +import { AzureCredentials, isCredentialsComplete } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; import { Select, Button, Field } from '@grafana/ui'; -import { isCredentialsComplete } from '../../credentials'; import { selectors } from '../../e2e/selectors'; -import { AzureCredentials, AzureDataSourceJsonData } from '../../types'; +import { AzureMonitorDataSourceJsonData } from '../../types'; export interface Props { - options: AzureDataSourceJsonData; + options: AzureMonitorDataSourceJsonData; credentials: AzureCredentials; getSubscriptions?: () => Promise; subscriptions: Array>; diff --git a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx index 5be22c99907..94ea592bf3d 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ConfigEditor/MonitorConfig.tsx @@ -1,19 +1,20 @@ import { useMemo, useState } from 'react'; import { useEffectOnce } from 'react-use'; +import { AzureCredentials } from '@grafana/azure-sdk'; import { SelectableValue } from '@grafana/data'; import { config } from '@grafana/runtime'; import { getCredentials, updateCredentials } from '../../credentials'; -import { AzureDataSourceSettings, AzureCredentials } from '../../types'; +import { AzureMonitorDataSourceSettings } from '../../types'; import { AzureCredentialsForm, getAzureCloudOptions } from './AzureCredentialsForm'; import { BasicLogsToggle } from './BasicLogsToggle'; import { DefaultSubscription } from './DefaultSubscription'; export interface Props { - options: AzureDataSourceSettings; - updateOptions: (optionsFunc: (options: AzureDataSourceSettings) => AzureDataSourceSettings) => void; + options: AzureMonitorDataSourceSettings; + updateOptions: (optionsFunc: (options: AzureMonitorDataSourceSettings) => AzureMonitorDataSourceSettings) => void; getSubscriptions: () => Promise>>; } @@ -26,7 +27,7 @@ export const MonitorConfig = (props: Props) => { if (!subscriptionId) { setSubscriptions([]); } - updateOptions((options) => + updateOptions((options: AzureMonitorDataSourceSettings) => updateCredentials({ ...options, jsonData: { ...options.jsonData, subscriptionId } }, credentials) ); }; diff --git a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx index 6c5fd3487dd..eaecfe57345 100644 --- a/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx @@ -9,7 +9,7 @@ import { Alert, Button, CodeEditor, Space } from '@grafana/ui'; import AzureMonitorDatasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; import { - AzureDataSourceJsonData, + AzureMonitorDataSourceJsonData, AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, @@ -28,7 +28,7 @@ import usePreparedQuery from './usePreparedQuery'; export type AzureMonitorQueryEditorProps = QueryEditorProps< AzureMonitorDatasource, AzureMonitorQuery, - AzureDataSourceJsonData + AzureMonitorDataSourceJsonData >; const QueryEditor = ({ diff --git a/public/app/plugins/datasource/azuremonitor/credentials.ts b/public/app/plugins/datasource/azuremonitor/credentials.ts index 440338d4b88..44aeb3af365 100644 --- a/public/app/plugins/datasource/azuremonitor/credentials.ts +++ b/public/app/plugins/datasource/azuremonitor/credentials.ts @@ -1,259 +1,74 @@ -import { getAzureClouds } from '@grafana/azure-sdk'; +import { + AzureCredentials, + getDatasourceCredentials, + getDefaultAzureCloud, + getClientSecret, + resolveLegacyCloudName, + updateDatasourceCredentials, +} from '@grafana/azure-sdk'; import { config } from '@grafana/runtime'; -import { - AadCurrentUserCredentials, - AzureAuthType, - AzureClientSecretCredentials, - AzureCloud, - AzureCredentials, - AzureDataSourceInstanceSettings, - AzureDataSourceSettings, - ConcealedSecret, -} from './types'; +import { AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceSettings } from './types'; -const concealed: ConcealedSecret = Symbol('Concealed client secret'); - -export function getAuthType(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): AzureAuthType { - if (!options.jsonData.azureAuthType) { - // If authentication type isn't explicitly specified and datasource has client credentials, - // then this is existing datasource which is configured for app registration (client secret) - if (options.jsonData.tenantId && options.jsonData.clientId) { - return 'clientsecret'; - } - - // For newly created datasource with no configuration, managed identity is the default authentication type - // if they are enabled in Grafana config - return config.azure.managedIdentityEnabled ? 'msi' : 'clientsecret'; +export function getCredentials( + options: AzureMonitorDataSourceSettings | AzureMonitorDataSourceInstanceSettings +): AzureCredentials { + // Try to get the credentials from the datasource settings, + // If not found, return the legacy azure monitor credentials if they exist or fallback to default credentials + const creds = getDatasourceCredentials(options); + if (creds) { + return creds; } - return options.jsonData.azureAuthType; -} - -function resolveLegacyCloudName(cloudName: string | undefined): string | undefined { - if (!cloudName) { - // if undefined, allow the code to fallback to calling getDefaultAzureCloud() since that has the complete logic for handling an empty cloud name - return undefined; - } - switch (cloudName) { - case 'azuremonitor': - return AzureCloud.Public; - case 'chinaazuremonitor': - return AzureCloud.China; - case 'govazuremonitor': - return AzureCloud.USGovernment; - default: - return cloudName; - } -} - -function getDefaultAzureCloud(): string { - const cloudName = resolveLegacyCloudName(config.azure.cloud); - - switch (cloudName) { - case AzureCloud.Public: - case AzureCloud.None: - return AzureCloud.Public; - case AzureCloud.China: - return AzureCloud.China; - case AzureCloud.USGovernment: - return AzureCloud.USGovernment; - default: - const cloudInfo = getAzureClouds(); - - for (const cloud of cloudInfo) { - if (cloud.name === config.azure.cloud) { - return cloud.name; - } - } - throw new Error(`The cloud '${config.azure.cloud}' is unsupported.`); - } -} - -export function getAzureCloud(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): string { - const authType = getAuthType(options); - switch (authType) { - case 'msi': - case 'workloadidentity': - // In case of managed identity and workload identity, the cloud is always same as where Grafana is hosted - return getDefaultAzureCloud(); - case 'clientsecret': - case 'currentuser': - return resolveLegacyCloudName(options.jsonData.cloudName) || getDefaultAzureCloud(); - } -} - -function getSecret(options: AzureDataSourceSettings): undefined | string | ConcealedSecret { - if (options.secureJsonFields.clientSecret) { - // The secret is concealed on server - return concealed; - } else { - const secret = options.secureJsonData?.clientSecret; - return typeof secret === 'string' && secret.length > 0 ? secret : undefined; - } -} - -export function isCredentialsComplete(credentials: AzureCredentials, ignoreSecret = false): boolean { - switch (credentials.authType) { - case 'msi': - case 'workloadidentity': - case 'currentuser': - return true; - case 'clientsecret': - return !!( - credentials.azureCloud && - credentials.tenantId && - credentials.clientId && - // When ignoreSecret is set we consider the credentials complete without checking the secret - !!(ignoreSecret || credentials.clientSecret) - ); - } -} - -export function instanceOfAzureCredential( - authType: AzureAuthType, - object?: AzureCredentials -): object is T { - if (!object) { - return false; - } - return object.authType === authType; -} - -export function getCredentials(options: AzureDataSourceSettings): AzureCredentials { - const authType = getAuthType(options); - const credentials = options.jsonData.azureCredentials; - switch (authType) { - case 'msi': - case 'workloadidentity': - if ( - (authType === 'msi' && config.azure.managedIdentityEnabled) || - (authType === 'workloadidentity' && config.azure.workloadIdentityEnabled) - ) { - return { - authType, - }; - } else { - // If authentication type is managed identity or workload identity but either method is disabled in Grafana config, - // then we should fallback to an empty app registration (client secret) configuration - return { - authType: 'clientsecret', - azureCloud: getDefaultAzureCloud(), - }; - } - case 'clientsecret': - return { - authType, - azureCloud: resolveLegacyCloudName(options.jsonData.cloudName) || getDefaultAzureCloud(), - tenantId: options.jsonData.tenantId, - clientId: options.jsonData.clientId, - clientSecret: getSecret(options), - }; - } - if (instanceOfAzureCredential(authType, credentials)) { - if (instanceOfAzureCredential('clientsecret', credentials.serviceCredentials)) { - const serviceCredentials = { ...credentials.serviceCredentials, clientSecret: getSecret(options) }; - return { - authType, - serviceCredentialsEnabled: credentials.serviceCredentialsEnabled, - serviceCredentials, - }; - } - return { - authType, - serviceCredentialsEnabled: credentials.serviceCredentialsEnabled, - serviceCredentials: credentials.serviceCredentials, - }; - } - return { - authType: 'clientsecret', - azureCloud: getDefaultAzureCloud(), - }; + return getLegacyCredentials(options) || getDefaultCredentials(); } export function updateCredentials( - options: AzureDataSourceSettings, + options: AzureMonitorDataSourceSettings, credentials: AzureCredentials -): AzureDataSourceSettings { - switch (credentials.authType) { - case 'msi': - case 'workloadidentity': - if (credentials.authType === 'msi' && !config.azure.managedIdentityEnabled) { - throw new Error('Managed Identity authentication is not enabled in Grafana config.'); - } - if (credentials.authType === 'workloadidentity' && !config.azure.workloadIdentityEnabled) { - throw new Error('Workload Identity authentication is not enabled in Grafana config.'); - } - - options = { - ...options, - jsonData: { - ...options.jsonData, - azureAuthType: credentials.authType, - azureCredentials: { - authType: credentials.authType, - }, - }, - }; - - return options; - - case 'clientsecret': - options = { - ...options, - jsonData: { - ...options.jsonData, - azureAuthType: credentials.authType, - cloudName: resolveLegacyCloudName(credentials.azureCloud) || getDefaultAzureCloud(), - tenantId: credentials.tenantId, - clientId: credentials.clientId, - azureCredentials: { - authType: credentials.authType, - azureCloud: resolveLegacyCloudName(credentials.azureCloud) || getDefaultAzureCloud(), - tenantId: credentials.tenantId, - clientId: credentials.clientId, - }, - }, - secureJsonData: { - ...options.secureJsonData, - clientSecret: typeof credentials.clientSecret === 'string' ? credentials.clientSecret : undefined, - }, - secureJsonFields: { - ...options.secureJsonFields, - clientSecret: typeof credentials.clientSecret === 'symbol', - }, - }; - } - if (instanceOfAzureCredential('currentuser', credentials)) { - const serviceCredentials = credentials.serviceCredentials; - let clientSecret: string | symbol | undefined; - if (instanceOfAzureCredential('clientsecret', serviceCredentials)) { - clientSecret = serviceCredentials.clientSecret; - // Do this to not expose the secret in unencrypted JSON data - delete serviceCredentials.clientSecret; - } - options = { - ...options, - jsonData: { - ...options.jsonData, - azureAuthType: credentials.authType, - azureCredentials: { - authType: 'currentuser', - serviceCredentialsEnabled: credentials.serviceCredentialsEnabled, - serviceCredentials, - }, - oauthPassThru: true, - disableGrafanaCache: true, - }, - secureJsonData: { - ...options.secureJsonData, - clientSecret: typeof clientSecret === 'string' ? clientSecret : undefined, - }, - secureJsonFields: { - ...options.secureJsonFields, - clientSecret: typeof clientSecret === 'symbol', - }, - }; - } - return options; +): AzureMonitorDataSourceSettings { + return updateDatasourceCredentials(options, credentials); +} + +function getLegacyCredentials( + options: AzureMonitorDataSourceSettings | AzureMonitorDataSourceInstanceSettings +): AzureCredentials | undefined { + try { + // If authentication type isn't explicitly specified and datasource has client credentials, + // then this is existing datasource which is configured for app registration (client secret) + if ( + options.jsonData.azureAuthType === 'clientsecret' || + (!options.jsonData.azureAuthType && options.jsonData.tenantId && options.jsonData.clientId) + ) { + return { + authType: 'clientsecret', + tenantId: options.jsonData.tenantId, + clientId: options.jsonData.clientId, + azureCloud: resolveLegacyCloudName(options.jsonData.cloudName) || getDefaultAzureCloud(), + clientSecret: getClientSecret(options), + }; + } + + // If the authentication type is not set, then no legacy credentials exist so return undefined + if (!options.jsonData.azureAuthType) { + return undefined; + } + + return { authType: options.jsonData.azureAuthType }; + } catch (e) { + if (e instanceof Error) { + console.error('Unable to restore legacy credentials: %s', e.message); + } + return undefined; + } +} + +function getDefaultCredentials(): AzureCredentials { + if (config.azure.managedIdentityEnabled) { + return { authType: 'msi' }; + } else if (config.azure.workloadIdentityEnabled) { + return { authType: 'workloadidentity' }; + } else { + return { authType: 'clientsecret', azureCloud: getDefaultAzureCloud() }; + } } diff --git a/public/app/plugins/datasource/azuremonitor/datasource.ts b/public/app/plugins/datasource/azuremonitor/datasource.ts index 79cf513998a..6b710baefd5 100644 --- a/public/app/plugins/datasource/azuremonitor/datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/datasource.ts @@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash'; import { forkJoin, Observable, of } from 'rxjs'; import { map } from 'rxjs/operators'; +import { AadCurrentUserCredentials, instanceOfAzureCredential, isCredentialsComplete } from '@grafana/azure-sdk'; import { DataFrame, DataQueryRequest, @@ -16,14 +17,13 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource'; import AzureResourceGraphDatasource from './azure_resource_graph/azure_resource_graph_datasource'; -import { instanceOfAzureCredential, isCredentialsComplete } from './credentials'; import ResourcePickerData from './resourcePicker/resourcePickerData'; -import { AadCurrentUserCredentials, AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from './types'; +import { AzureMonitorDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from './types'; import migrateAnnotation from './utils/migrateAnnotation'; import migrateQuery from './utils/migrateQuery'; import { VariableSupport } from './variables'; -export default class Datasource extends DataSourceWithBackend { +export default class Datasource extends DataSourceWithBackend { annotations = { prepareAnnotation: migrateAnnotation, }; @@ -42,7 +42,7 @@ export default class Datasource extends DataSourceWithBackend; constructor( - instanceSettings: DataSourceInstanceSettings, + instanceSettings: DataSourceInstanceSettings, private readonly templateSrv: TemplateSrv = getTemplateSrv() ) { super(instanceSettings); diff --git a/public/app/plugins/datasource/azuremonitor/module.ts b/public/app/plugins/datasource/azuremonitor/module.ts index 04264961804..547c9406307 100644 --- a/public/app/plugins/datasource/azuremonitor/module.ts +++ b/public/app/plugins/datasource/azuremonitor/module.ts @@ -6,9 +6,9 @@ import AzureMonitorQueryEditor from './components/QueryEditor'; import Datasource from './datasource'; import pluginJson from './plugin.json'; import { trackAzureMonitorDashboardLoaded } from './tracking'; -import { AzureMonitorQuery, AzureDataSourceJsonData, AzureQueryType, ResultFormat } from './types'; +import { AzureMonitorQuery, AzureMonitorDataSourceJsonData, AzureQueryType, ResultFormat } from './types'; -export const plugin = new DataSourcePlugin(Datasource) +export const plugin = new DataSourcePlugin(Datasource) .setConfigEditor(ConfigEditor) .setQueryEditor(AzureMonitorQueryEditor); diff --git a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts index b3b6b73b171..c0f9bd9500b 100644 --- a/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/azuremonitor/resourcePicker/resourcePickerData.ts @@ -1,4 +1,3 @@ -import { DataSourceInstanceSettings } from '@grafana/data'; import { DataSourceWithBackend, reportInteraction } from '@grafana/runtime'; import { logsResourceTypes, resourceTypeDisplayNames, resourceTypes } from '../azureMetadata'; @@ -13,7 +12,8 @@ import { resourceToString, } from '../components/ResourcePicker/utils'; import { - AzureDataSourceJsonData, + AzureMonitorDataSourceInstanceSettings, + AzureMonitorDataSourceJsonData, AzureGraphResponse, AzureMonitorResource, AzureMonitorQuery, @@ -31,14 +31,17 @@ const logsSupportedResourceTypesKusto = logsResourceTypes.map((v) => `"${v}"`).j export type ResourcePickerQueryType = 'logs' | 'metrics' | 'traces'; -export default class ResourcePickerData extends DataSourceWithBackend { +export default class ResourcePickerData extends DataSourceWithBackend< + AzureMonitorQuery, + AzureMonitorDataSourceJsonData +> { private resourcePath: string; resultLimit = 200; azureMonitorDatasource; supportedMetricNamespaces = ''; constructor( - instanceSettings: DataSourceInstanceSettings, + instanceSettings: AzureMonitorDataSourceInstanceSettings, azureMonitorDatasource: AzureMonitorDatasource ) { super(instanceSettings); diff --git a/public/app/plugins/datasource/azuremonitor/types/types.ts b/public/app/plugins/datasource/azuremonitor/types/types.ts index 7dddddcd63b..908c7066d0f 100644 --- a/public/app/plugins/datasource/azuremonitor/types/types.ts +++ b/public/app/plugins/datasource/azuremonitor/types/types.ts @@ -1,21 +1,18 @@ import { ScalarParameter, TabularParameter, Function, EntityGroup } from '@kusto/monaco-kusto'; -import { - DataSourceInstanceSettings, - DataSourceJsonData, - DataSourceSettings, - PanelData, - SelectableValue, - TimeRange, -} from '@grafana/data'; +import { AzureDataSourceSecureJsonData, AzureDataSourceJsonData } from '@grafana/azure-sdk'; +import { DataSourceInstanceSettings, DataSourceSettings, PanelData, SelectableValue, TimeRange } from '@grafana/data'; import Datasource from '../datasource'; import { AzureLogAnalyticsMetadataTable } from './logAnalyticsMetadata'; import { AzureMonitorQuery, ResultFormat } from './query'; -export type AzureDataSourceSettings = DataSourceSettings; -export type AzureDataSourceInstanceSettings = DataSourceInstanceSettings; +export type AzureMonitorDataSourceSettings = DataSourceSettings< + AzureMonitorDataSourceJsonData, + AzureMonitorDataSourceSecureJsonData +>; +export type AzureMonitorDataSourceInstanceSettings = DataSourceInstanceSettings; export interface DatasourceValidationResult { status: 'success' | 'error'; @@ -23,64 +20,9 @@ export interface DatasourceValidationResult { title?: string; } -/** - * Azure clouds known to Azure Monitor. - */ -export enum AzureCloud { - Public = 'AzureCloud', - China = 'AzureChinaCloud', - USGovernment = 'AzureUSGovernment', - None = '', -} - -export type AzureAuthType = 'msi' | 'clientsecret' | 'workloadidentity' | 'currentuser'; - -export type ConcealedSecret = symbol; - -interface AzureCredentialsBase { - authType: AzureAuthType; -} - -export interface AzureManagedIdentityCredentials extends AzureCredentialsBase { - authType: 'msi'; -} - -export interface AzureWorkloadIdentityCredentials extends AzureCredentialsBase { - authType: 'workloadidentity'; -} - -export interface AzureClientSecretCredentials extends AzureCredentialsBase { - authType: 'clientsecret'; - azureCloud?: string; - tenantId?: string; - clientId?: string; - clientSecret?: string | ConcealedSecret; -} -export interface AadCurrentUserCredentials extends AzureCredentialsBase { - authType: 'currentuser'; - serviceCredentials?: - | AzureClientSecretCredentials - | AzureManagedIdentityCredentials - | AzureWorkloadIdentityCredentials; - serviceCredentialsEnabled?: boolean; -} - -export type AzureCredentials = - | AadCurrentUserCredentials - | AzureManagedIdentityCredentials - | AzureClientSecretCredentials - | AzureWorkloadIdentityCredentials; - -export interface AzureDataSourceJsonData extends DataSourceJsonData { - cloudName: string; - azureAuthType?: AzureAuthType; - +export interface AzureMonitorDataSourceJsonData extends AzureDataSourceJsonData { // monitor - tenantId?: string; - clientId?: string; subscriptionId?: string; - oauthPassThru?: boolean; - azureCredentials?: AzureCredentials; basicLogsEnabled?: boolean; // logs @@ -101,8 +43,7 @@ export interface AzureDataSourceJsonData extends DataSourceJsonData { enableSecureSocksProxy?: boolean; } -export interface AzureDataSourceSecureJsonData { - clientSecret?: string; +export interface AzureMonitorDataSourceSecureJsonData extends AzureDataSourceSecureJsonData { appInsightsApiKey?: string; }