Azure: Unify credentials in frontend (#95354)

* init

* fix lint

* fix lint

* lint

* update version

* fix
This commit is contained in:
Younjin Song
2024-11-12 05:36:48 -05:00
committed by GitHub
parent 2f58311eea
commit 44d206c272
20 changed files with 155 additions and 386 deletions

View File

@@ -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<AzureDataSourceJsonData>;
instanceSettings: AzureMonitorDataSourceInstanceSettings;
templateSrv: TemplateSrv;
datasource: Datasource;
getResource: jest.Mock;

View File

@@ -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<AzureDataSourceSettings>,
overrides?: DeepPartial<AzureMonitorDataSourceSettings>,
secureJsonFieldsOverrides?: KeyValue<boolean>
): AzureDataSourceSettings => {
): AzureMonitorDataSourceSettings => {
return {
id: 1,
uid: 'uid',

View File

@@ -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<DataSourceInstanceSettings>
): AzureDataSourceInstanceSettings => {
): AzureMonitorDataSourceInstanceSettings => {
const metaOverrides = overrides?.meta;
return {
url: '/ds/1',

View File

@@ -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<AzureDataSourceJsonData>,
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.',

View File

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

View File

@@ -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<AzureMonitorQuery, AzureDataSourceJsonData> {
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<AzureM
declare resourceName: string;
constructor(
private instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
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<AzureM
}
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.',

View File

@@ -4,12 +4,12 @@ import _ from 'lodash';
import { ScopedVars } from '@grafana/data';
import { getTemplateSrv, DataSourceWithBackend } from '@grafana/runtime';
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureQueryType } from '../types';
import { AzureMonitorQuery, AzureMonitorDataSourceJsonData, AzureQueryType } from '../types';
import { interpolateVariable } from '../utils/common';
export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
AzureMonitorQuery,
AzureDataSourceJsonData
AzureMonitorDataSourceJsonData
> {
filterQuery(item: AzureMonitorQuery): boolean {
return !!item.azureResourceGraph?.query && !!item.subscriptions && item.subscriptions.length > 0;

View File

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

View File

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

View File

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

View File

@@ -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<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
export type Props = DataSourcePluginOptionsEditorProps<
AzureMonitorDataSourceJsonData,
AzureMonitorDataSourceSecureJsonData
>;
interface ErrorMessage {
title: string;
@@ -43,7 +46,9 @@ export class ConfigEditor extends PureComponent<Props, State> {
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<Props, State> {
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);
});

View File

@@ -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<SelectableValue<Exclude<AzureAuthType, 'currentuser'>>> = [
let opts: Array<SelectableValue<FallbackCredentialAuthTypeOptions>> = [
{
value: 'clientsecret',
label: 'App Registration',
@@ -57,7 +57,7 @@ export const CurrentUserFallbackCredentials = (props: Props) => {
return opts;
}, [managedIdentityEnabled, workloadIdentityEnabled]);
const onAuthTypeChange = (selected: SelectableValue<Exclude<AzureAuthType, 'currentuser'>>) => {
const onAuthTypeChange = (selected: SelectableValue<FallbackCredentialAuthTypeOptions>) => {
const defaultAuthType = managedIdentityEnabled
? 'msi'
: workloadIdentityEnabled

View File

@@ -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<SelectableValue[]>;
subscriptions: Array<SelectableValue<string>>;

View File

@@ -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<Array<SelectableValue<string>>>;
}
@@ -26,7 +27,7 @@ export const MonitorConfig = (props: Props) => {
if (!subscriptionId) {
setSubscriptions([]);
}
updateOptions((options) =>
updateOptions((options: AzureMonitorDataSourceSettings) =>
updateCredentials({ ...options, jsonData: { ...options.jsonData, subscriptionId } }, credentials)
);
};

View File

@@ -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 = ({

View File

@@ -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<T extends AzureCredentials>(
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<AadCurrentUserCredentials>(authType, credentials)) {
if (instanceOfAzureCredential<AzureClientSecretCredentials>('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<AadCurrentUserCredentials>('currentuser', credentials)) {
const serviceCredentials = credentials.serviceCredentials;
let clientSecret: string | symbol | undefined;
if (instanceOfAzureCredential<AzureClientSecretCredentials>('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() };
}
}

View File

@@ -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<AzureMonitorQuery, AzureDataSourceJsonData> {
export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery, AzureMonitorDataSourceJsonData> {
annotations = {
prepareAnnotation: migrateAnnotation,
};
@@ -42,7 +42,7 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
declare optionsKey: Record<AzureQueryType, string>;
constructor(
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
instanceSettings: DataSourceInstanceSettings<AzureMonitorDataSourceJsonData>,
private readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);

View File

@@ -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, AzureMonitorQuery, AzureDataSourceJsonData>(Datasource)
export const plugin = new DataSourcePlugin<Datasource, AzureMonitorQuery, AzureMonitorDataSourceJsonData>(Datasource)
.setConfigEditor(ConfigEditor)
.setQueryEditor(AzureMonitorQueryEditor);

View File

@@ -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<AzureMonitorQuery, AzureDataSourceJsonData> {
export default class ResourcePickerData extends DataSourceWithBackend<
AzureMonitorQuery,
AzureMonitorDataSourceJsonData
> {
private resourcePath: string;
resultLimit = 200;
azureMonitorDatasource;
supportedMetricNamespaces = '';
constructor(
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>,
instanceSettings: AzureMonitorDataSourceInstanceSettings,
azureMonitorDatasource: AzureMonitorDatasource
) {
super(instanceSettings);

View File

@@ -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<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
export type AzureDataSourceInstanceSettings = DataSourceInstanceSettings<AzureDataSourceJsonData>;
export type AzureMonitorDataSourceSettings = DataSourceSettings<
AzureMonitorDataSourceJsonData,
AzureMonitorDataSourceSecureJsonData
>;
export type AzureMonitorDataSourceInstanceSettings = DataSourceInstanceSettings<AzureMonitorDataSourceJsonData>;
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;
}