AzureMonitor: Managed Identity configuration UI (#34170)

* Basic UI for Managed Identity

* Credentials logic

* Fix datasource validation

* Do not offer Managed Identity for Log Analytics

* Logic fixes

* Show Log Analytics credentials only for App Registration

* Fix tests

* Datasource validation refactoring
This commit is contained in:
Sergey Kostrukov
2021-05-19 12:48:53 -07:00
committed by GitHub
parent e52f718a87
commit afbd4dec52
14 changed files with 962 additions and 739 deletions

View File

@@ -3,7 +3,7 @@ import { getBackendSrv, getTemplateSrv, DataSourceWithBackend } from '@grafana/r
import { isString } from 'lodash';
import TimegrainConverter from '../time_grain_converter';
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType } from '../types';
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult } from '../types';
import ResponseParser from './response_parser';
import { getAzureCloud } from '../credentials';
import { getAppInsightsApiRoute } from '../api/routes';
@@ -132,10 +132,10 @@ export default class AppInsightsDatasource extends DataSourceWithBackend<AzureMo
return null;
}
testDatasource() {
testDatasource(): Promise<DatasourceValidationResult> {
const url = `${this.baseUrl}/metrics/metadata`;
return this.doRequest(url)
.then((response: any) => {
.then<DatasourceValidationResult>((response: any) => {
if (response.status === 200) {
return {
status: 'success',

View File

@@ -1,7 +1,13 @@
import { map } from 'lodash';
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
import ResponseParser, { transformMetadataToKustoSchema } from './response_parser';
import { AzureMonitorQuery, AzureDataSourceJsonData, AzureLogsVariable, AzureQueryType } from '../types';
import {
AzureMonitorQuery,
AzureDataSourceJsonData,
AzureLogsVariable,
AzureQueryType,
DatasourceValidationResult,
} from '../types';
import {
DataQueryRequest,
DataQueryResponse,
@@ -12,7 +18,7 @@ import {
import { getBackendSrv, getTemplateSrv, DataSourceWithBackend, FetchResponse } from '@grafana/runtime';
import { Observable, from } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { getAzureCloud } from '../credentials';
import { getAuthType, getAzureCloud } from '../credentials';
import { getLogAnalyticsApiRoute, getLogAnalyticsManagementApiRoute } from '../api/routes';
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
@@ -349,8 +355,8 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
}
// TODO: update to be resource-centric
testDatasource(): Promise<any> {
const validationError = this.isValidConfig();
testDatasource(): Promise<DatasourceValidationResult> {
const validationError = this.validateDatasource();
if (validationError) {
return Promise.resolve(validationError);
}
@@ -361,7 +367,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
return this.doRequest(url);
})
.then((response: any) => {
.then<DatasourceValidationResult>((response: any) => {
if (response.status === 200) {
return {
status: 'success',
@@ -404,36 +410,36 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
return message;
}
isValidConfig() {
if (this.instanceSettings.jsonData.azureLogAnalyticsSameAs) {
return undefined;
private validateDatasource(): DatasourceValidationResult | undefined {
const authType = getAuthType(this.instanceSettings);
if (authType === 'clientsecret') {
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsTenantId)) {
return {
status: 'error',
message: 'The Tenant Id field is required.',
};
}
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsClientId)) {
return {
status: 'error',
message: 'The Client Id field is required.',
};
}
}
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsSubscriptionId)) {
if (!this.isValidConfigField(this.subscriptionId)) {
return {
status: 'error',
message: 'The Subscription Id field is required.',
};
}
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsTenantId)) {
return {
status: 'error',
message: 'The Tenant Id field is required.',
};
}
if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsClientId)) {
return {
status: 'error',
message: 'The Client Id field is required.',
};
}
return undefined;
}
isValidConfigField(field: string | undefined) {
return field && field.length > 0;
private isValidConfigField(field: string | undefined): boolean {
return typeof field === 'string' && field.length > 0;
}
}

View File

@@ -11,6 +11,7 @@ import {
AzureQueryType,
AzureMonitorMetricsMetadataResponse,
AzureMetricQuery,
DatasourceValidationResult,
} from '../types';
import {
DataSourceInstanceSettings,
@@ -25,7 +26,7 @@ import { from, Observable } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getAzureCloud } from '../credentials';
import { getAuthType, getAzureCloud } from '../credentials';
import { getManagementApiRoute } from '../api/routes';
const defaultDropdownValue = 'select';
@@ -458,24 +459,15 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
});
}
testDatasource(): Promise<any> {
if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
return Promise.resolve({
status: 'error',
message: 'The Tenant Id field is required.',
});
}
if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
return Promise.resolve({
status: 'error',
message: 'The Client Id field is required.',
});
testDatasource(): Promise<DatasourceValidationResult> {
const validationError = this.validateDatasource();
if (validationError) {
return Promise.resolve(validationError);
}
const url = `${this.baseUrl}?api-version=2019-03-01`;
return this.doRequest(url)
.then((response: any) => {
.then<DatasourceValidationResult>((response: any) => {
if (response.status === 200) {
return {
status: 'success',
@@ -509,8 +501,37 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
});
}
isValidConfigField(field?: string) {
return field && field.length > 0;
private validateDatasource(): DatasourceValidationResult | undefined {
const authType = getAuthType(this.instanceSettings);
if (authType === 'clientsecret') {
if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
return {
status: 'error',
message: 'The Tenant Id field is required.',
};
}
if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
return {
status: 'error',
message: 'The Client Id field is required.',
};
}
}
if (!this.isValidConfigField(this.subscriptionId)) {
return {
status: 'error',
message: 'The Subscription Id field is required.',
};
}
return undefined;
}
private isValidConfigField(field?: string): boolean {
return typeof field === 'string' && field.length > 0;
}
doRequest<T = any>(url: string, maxRetries = 1): Promise<FetchResponse<T>> {

View File

@@ -27,6 +27,8 @@ export const AnalyticsConfig: FunctionComponent<Props> = (props: Props) => {
? props.options.jsonData.logAnalyticsSubscriptionId
: props.options.jsonData.subscriptionId;
const credentialsEnabled = primaryCredentials.authType === 'clientsecret';
const hasRequiredFields =
subscriptionId &&
(logAnalyticsCredentials
@@ -127,32 +129,41 @@ export const AnalyticsConfig: FunctionComponent<Props> = (props: Props) => {
}),
};
const showSameAsHelpMsg = sameAsSwitched && !primaryCredentials.clientSecret;
const showSameAsHelpMsg =
credentialsEnabled &&
sameAsSwitched &&
primaryCredentials.authType === 'clientsecret' &&
!primaryCredentials.clientSecret;
return (
<>
<h3 className="page-heading">Azure Monitor Logs Details</h3>
<Switch
label="Same details as Azure Monitor API"
checked={!logAnalyticsCredentials}
onChange={onLogAnalyticsSameAsChange}
{...tooltipAttribute}
/>
{showSameAsHelpMsg && (
<div className="grafana-info-box m-t-2">
<div className="alert-body">
<p>Re-enter your Azure Monitor Client Secret to use this setting.</p>
</div>
</div>
)}
{logAnalyticsCredentials && (
<AzureCredentialsForm
credentials={logAnalyticsCredentials}
defaultSubscription={subscriptionId}
onCredentialsChange={onCredentialsChange}
onDefaultSubscriptionChange={onLogAnalyticsDefaultSubscriptionChange}
getSubscriptions={getSubscriptions}
/>
<h3 className="page-heading">Azure Monitor Logs</h3>
{credentialsEnabled && (
<>
<Switch
label="Same details as Azure Monitor API"
checked={!logAnalyticsCredentials}
onChange={onLogAnalyticsSameAsChange}
{...tooltipAttribute}
/>
{showSameAsHelpMsg && (
<div className="grafana-info-box m-t-2">
<div className="alert-body">
<p>Re-enter your Azure Monitor Client Secret to use this setting.</p>
</div>
</div>
)}
{logAnalyticsCredentials && (
<AzureCredentialsForm
managedIdentityEnabled={false}
credentials={logAnalyticsCredentials}
defaultSubscription={subscriptionId}
onCredentialsChange={onCredentialsChange}
onDefaultSubscriptionChange={onLogAnalyticsDefaultSubscriptionChange}
getSubscriptions={getSubscriptions}
/>
)}
</>
)}
<div className="gf-form-group">
<div className="gf-form-inline">

View File

@@ -4,7 +4,9 @@ import AzureCredentialsForm, { Props } from './AzureCredentialsForm';
const setup = (propsFunc?: (props: Props) => Props) => {
let props: Props = {
managedIdentityEnabled: false,
credentials: {
authType: 'clientsecret',
azureCloud: 'azuremonitor',
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
@@ -39,6 +41,7 @@ describe('Render', () => {
const wrapper = setup((props) => ({
...props,
credentials: {
authType: 'clientsecret',
azureCloud: 'azuremonitor',
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
@@ -52,6 +55,7 @@ describe('Render', () => {
const wrapper = setup((props) => ({
...props,
credentials: {
authType: 'clientsecret',
azureCloud: 'azuremonitor',
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',

View File

@@ -1,11 +1,12 @@
import React, { ChangeEvent, FunctionComponent, useEffect, useReducer, useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { InlineFormLabel, LegacyForms, Button } from '@grafana/ui';
import { AzureCredentials } from '../types';
import { AzureAuthType, AzureCredentials } from '../types';
import { isCredentialsComplete } from '../credentials';
const { Select, Input } = LegacyForms;
export interface Props {
managedIdentityEnabled: boolean;
credentials: AzureCredentials;
defaultSubscription?: string;
azureCloudOptions?: SelectableValue[];
@@ -14,6 +15,17 @@ export interface Props {
getSubscriptions?: () => Promise<SelectableValue[]>;
}
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
{
value: 'msi',
label: 'Managed Identity',
},
{
value: 'clientsecret',
label: 'App Registration',
},
];
export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) => {
const {
credentials,
@@ -61,8 +73,18 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
}
};
const onAzureCloudChange = (selected: SelectableValue<string>) => {
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
if (onCredentialsChange) {
const updated: AzureCredentials = {
...credentials,
authType: selected.value || 'msi',
};
onCredentialsChange(updated);
}
};
const onAzureCloudChange = (selected: SelectableValue<string>) => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
azureCloud: selected.value,
@@ -72,7 +94,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
};
const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onCredentialsChange) {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
tenantId: event.target.value,
@@ -82,7 +104,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
};
const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onCredentialsChange) {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
clientId: event.target.value,
@@ -92,7 +114,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
};
const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onCredentialsChange) {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
clientSecret: event.target.value,
@@ -102,7 +124,7 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
};
const onClientSecretReset = () => {
if (onCredentialsChange) {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
clientSecret: '',
@@ -118,111 +140,128 @@ export const AzureCredentialsForm: FunctionComponent<Props> = (props: Props) =>
};
return (
<>
<div className="gf-form-group">
{azureCloudOptions && (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12" tooltip="Choose an Azure Cloud.">
Azure Cloud
</InlineFormLabel>
<Select
className="width-15"
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions}
onChange={onAzureCloudChange}
/>
</div>
</div>
)}
<div className="gf-form-group">
{props.managedIdentityEnabled && (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Directory (tenant) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''}
onChange={onTenantIdChange}
/>
</div>
<InlineFormLabel className="width-12" tooltip="Choose the type of authentication to Azure services">
Authentication
</InlineFormLabel>
<Select
className="width-15"
value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
options={authTypeOptions}
onChange={onAuthTypeChange}
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Application (client) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={onClientIdChange}
/>
</div>
</div>
</div>
{typeof credentials.clientSecret === 'symbol' ? (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<Input className="width-25" placeholder="configured" disabled={true} />
</div>
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button variant="secondary" type="button" onClick={onClientSecretReset}>
reset
</Button>
)}
{credentials.authType === 'clientsecret' && (
<>
{azureCloudOptions && (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12" tooltip="Choose an Azure Cloud">
Azure Cloud
</InlineFormLabel>
<Select
className="width-15"
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions}
onChange={onAzureCloudChange}
/>
</div>
</div>
</div>
) : (
)}
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<InlineFormLabel className="width-12">Directory (tenant) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientSecret || ''}
onChange={onClientSecretChange}
value={credentials.tenantId || ''}
onChange={onTenantIdChange}
/>
</div>
</div>
</div>
)}
{getSubscriptions && onDefaultSubscriptionChange && (
<>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
<div className="width-25">
<Select
value={subscriptions.find((opt) => opt.value === defaultSubscription)}
options={subscriptions}
onChange={onSubscriptionChange}
/>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Application (client) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={onClientIdChange}
/>
</div>
</div>
</div>
{typeof credentials.clientSecret === 'symbol' ? (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<Input className="width-25" placeholder="configured" disabled={true} />
</div>
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button
variant="secondary"
size="sm"
type="button"
onClick={onLoadSubscriptions}
disabled={!hasRequiredFields}
>
Load Subscriptions
<Button variant="secondary" type="button" onClick={onClientSecretReset}>
reset
</Button>
</div>
</div>
</div>
</>
)}
</div>
</>
) : (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientSecret || ''}
onChange={onClientSecretChange}
/>
</div>
</div>
</div>
)}
</>
)}
{getSubscriptions && onDefaultSubscriptionChange && (
<>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
<div className="width-25">
<Select
value={subscriptions.find((opt) => opt.value === defaultSubscription)}
options={subscriptions}
onChange={onSubscriptionChange}
/>
</div>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button
variant="secondary"
size="sm"
type="button"
onClick={onLoadSubscriptions}
disabled={!hasRequiredFields}
>
Load Subscriptions
</Button>
</div>
</div>
</div>
</>
)}
</div>
);
};

View File

@@ -22,7 +22,7 @@ export class InsightsConfig extends PureComponent<Props> {
const { options, onUpdateJsonDataOption, onUpdateSecureJsonDataOption } = this.props;
return (
<>
<h3 className="page-heading">Azure Application Insights Details</h3>
<h3 className="page-heading">Azure Application Insights</h3>
<div className="gf-form-group">
{options.secureJsonFields.appInsightsApiKey ? (
<div className="gf-form-inline">

View File

@@ -1,5 +1,6 @@
import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { AzureCredentialsForm } from './AzureCredentialsForm';
import { AzureDataSourceSettings, AzureCredentials } from '../types';
import { getCredentials, updateCredentials, isLogAnalyticsSameAs } from '../credentials';
@@ -50,8 +51,9 @@ export const MonitorConfig: FunctionComponent<Props> = (props: Props) => {
return (
<>
<h3 className="page-heading">Azure Monitor Metrics Details</h3>
<h3 className="page-heading">Authentication</h3>
<AzureCredentialsForm
managedIdentityEnabled={config.azure.managedIdentityEnabled}
credentials={credentials}
defaultSubscription={subscriptionId}
azureCloudOptions={azureClouds}

View File

@@ -5,7 +5,7 @@ exports[`Render should disable log analytics credentials form 1`] = `
<h3
className="page-heading"
>
Azure Monitor Logs Details
Azure Monitor Logs
</h3>
<Switch
checked={true}
@@ -89,7 +89,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
<h3
className="page-heading"
>
Azure Monitor Logs Details
Azure Monitor Logs
</h3>
<Switch
checked={false}
@@ -99,6 +99,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
<AzureCredentialsForm
credentials={
Object {
"authType": "clientsecret",
"azureCloud": "azuremonitor",
"clientId": "44693801-6ee6-49de-9b2d-9106972f9572",
"clientSecret": undefined,
@@ -107,6 +108,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
}
defaultSubscription="e3fe4fde-ad5e-4d60-9974-e2f3562ffdf2"
getSubscriptions={[MockFunction]}
managedIdentityEnabled={false}
onCredentialsChange={[Function]}
onDefaultSubscriptionChange={[Function]}
/>
@@ -186,7 +188,7 @@ exports[`Render should render component 1`] = `
<h3
className="page-heading"
>
Azure Monitor Logs Details
Azure Monitor Logs
</h3>
<Switch
checked={false}
@@ -196,6 +198,7 @@ exports[`Render should render component 1`] = `
<AzureCredentialsForm
credentials={
Object {
"authType": "clientsecret",
"azureCloud": "azuremonitor",
"clientId": undefined,
"clientSecret": undefined,
@@ -203,6 +206,7 @@ exports[`Render should render component 1`] = `
}
}
getSubscriptions={[MockFunction]}
managedIdentityEnabled={false}
onCredentialsChange={[Function]}
onDefaultSubscriptionChange={[Function]}
/>

View File

@@ -5,7 +5,7 @@ exports[`Render should disable insights api key input 1`] = `
<h3
className="page-heading"
>
Azure Application Insights Details
Azure Application Insights
</h3>
<div
className="gf-form-group"
@@ -73,7 +73,7 @@ exports[`Render should enable insights api key input 1`] = `
<h3
className="page-heading"
>
Azure Application Insights Details
Azure Application Insights
</h3>
<div
className="gf-form-group"
@@ -130,7 +130,7 @@ exports[`Render should render component 1`] = `
<h3
className="page-heading"
>
Azure Application Insights Details
Azure Application Insights
</h3>
<div
className="gf-form-group"

View File

@@ -1,9 +1,57 @@
import { AzureCredentials, AzureDataSourceInstanceSettings, AzureDataSourceSettings, ConcealedSecret } from './types';
import { config } from '@grafana/runtime';
import {
AzureAuthType,
AzureCloud,
AzureCredentials,
AzureDataSourceInstanceSettings,
AzureDataSourceSettings,
ConcealedSecret,
} 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';
}
return options.jsonData.azureAuthType;
}
function getDefaultAzureCloud(): string {
switch (config.azure.cloud) {
case AzureCloud.Public:
case AzureCloud.None:
case undefined:
return 'azuremonitor';
case AzureCloud.China:
return 'chinaazuremonitor';
case AzureCloud.USGovernment:
return 'govazuremonitor';
case AzureCloud.Germany:
return 'germanyazuremonitor';
default:
throw new Error(`The cloud '${config.azure.cloud}' not supported.`);
}
}
export function getAzureCloud(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): string {
return options.jsonData.cloudName || 'azuremonitor';
const authType = getAuthType(options);
switch (authType) {
case 'msi':
// In case of managed identity, the cloud is always same as where Grafana is hosted
return getDefaultAzureCloud();
case 'clientsecret':
return options.jsonData.cloudName || getDefaultAzureCloud();
}
}
function getSecret(options: AzureDataSourceSettings): undefined | string | ConcealedSecret {
@@ -26,30 +74,62 @@ function getLogAnalyticsSecret(options: AzureDataSourceSettings): undefined | st
}
}
export function isLogAnalyticsSameAs(options: AzureDataSourceSettings): boolean {
export function isLogAnalyticsSameAs(options: AzureDataSourceSettings | AzureDataSourceInstanceSettings): boolean {
return typeof options.jsonData.azureLogAnalyticsSameAs !== 'boolean' || options.jsonData.azureLogAnalyticsSameAs;
}
export function isCredentialsComplete(credentials: AzureCredentials) {
return !!(credentials.tenantId && credentials.clientId && credentials.clientSecret);
export function isCredentialsComplete(credentials: AzureCredentials): boolean {
switch (credentials.authType) {
case 'msi':
return true;
case 'clientsecret':
return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret);
}
}
export function getCredentials(options: AzureDataSourceSettings): AzureCredentials {
return {
azureCloud: getAzureCloud(options),
tenantId: options.jsonData.tenantId,
clientId: options.jsonData.clientId,
clientSecret: getSecret(options),
};
const authType = getAuthType(options);
switch (authType) {
case 'msi':
if (config.azure.managedIdentityEnabled) {
return {
authType: 'msi',
};
} else {
// If authentication type is managed identity but managed identities were 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: 'clientsecret',
azureCloud: options.jsonData.cloudName || getDefaultAzureCloud(),
tenantId: options.jsonData.tenantId,
clientId: options.jsonData.clientId,
clientSecret: getSecret(options),
};
}
}
export function getLogAnalyticsCredentials(options: AzureDataSourceSettings): AzureCredentials | undefined {
const authType = getAuthType(options);
if (authType !== 'clientsecret') {
// Only app registration (client secret) authentication supports different credentials for Log Analytics
// for backward compatibility
return undefined;
}
if (isLogAnalyticsSameAs(options)) {
return undefined;
}
return {
azureCloud: getAzureCloud(options),
authType: 'clientsecret',
azureCloud: options.jsonData.cloudName || getDefaultAzureCloud(),
tenantId: options.jsonData.logAnalyticsTenantId,
clientId: options.jsonData.logAnalyticsClientId,
clientSecret: getLogAnalyticsSecret(options),
@@ -60,57 +140,83 @@ export function updateCredentials(
options: AzureDataSourceSettings,
credentials: AzureCredentials
): AzureDataSourceSettings {
options = {
...options,
jsonData: {
...options.jsonData,
cloudName: credentials.azureCloud || 'azuremonitor',
tenantId: credentials.tenantId,
clientId: credentials.clientId,
},
secureJsonData: {
...options.secureJsonData,
clientSecret:
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
? credentials.clientSecret
: undefined,
},
secureJsonFields: {
...options.secureJsonFields,
clientSecret: typeof credentials.clientSecret === 'symbol',
},
};
switch (credentials.authType) {
case 'msi':
if (!config.azure.managedIdentityEnabled) {
throw new Error('Managed Identity authentication is not enabled in Grafana config.');
}
if (isLogAnalyticsSameAs(options)) {
options = updateLogAnalyticsCredentials(options, credentials);
options = {
...options,
jsonData: {
...options.jsonData,
azureAuthType: 'msi',
},
};
if (!isLogAnalyticsSameAs(options)) {
options = updateLogAnalyticsSameAs(options, true);
}
return options;
case 'clientsecret':
options = {
...options,
jsonData: {
...options.jsonData,
azureAuthType: 'clientsecret',
cloudName: credentials.azureCloud || getDefaultAzureCloud(),
tenantId: credentials.tenantId,
clientId: credentials.clientId,
},
secureJsonData: {
...options.secureJsonData,
clientSecret:
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
? credentials.clientSecret
: undefined,
},
secureJsonFields: {
...options.secureJsonFields,
clientSecret: typeof credentials.clientSecret === 'object',
},
};
if (isLogAnalyticsSameAs(options)) {
options = updateLogAnalyticsCredentials(options, credentials);
}
return options;
}
return options;
}
export function updateLogAnalyticsCredentials(
options: AzureDataSourceSettings,
credentials: AzureCredentials
): AzureDataSourceSettings {
options = {
...options,
jsonData: {
...options.jsonData,
logAnalyticsTenantId: credentials.tenantId,
logAnalyticsClientId: credentials.clientId,
},
secureJsonData: {
...options.secureJsonData,
logAnalyticsClientSecret:
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
? credentials.clientSecret
: undefined,
},
secureJsonFields: {
...options.secureJsonFields,
logAnalyticsClientSecret: typeof credentials.clientSecret === 'symbol',
},
};
// Log Analytics credentials only used if primary credentials are App Registration (client secret)
if (credentials.authType === 'clientsecret') {
options = {
...options,
jsonData: {
...options.jsonData,
logAnalyticsTenantId: credentials.tenantId,
logAnalyticsClientId: credentials.clientId,
},
secureJsonData: {
...options.secureJsonData,
logAnalyticsClientSecret:
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
? credentials.clientSecret
: undefined,
},
secureJsonFields: {
...options.secureJsonFields,
logAnalyticsClientSecret: typeof credentials.clientSecret === 'symbol',
},
};
}
return options;
}
@@ -130,16 +236,19 @@ export function updateLogAnalyticsSameAs(options: AzureDataSourceSettings, sameA
// Get the primary credentials
let credentials = getCredentials(options);
// Check whether the client secret is concealed
if (typeof credentials.clientSecret === 'symbol') {
// Log Analytics credentials need to be synchronized but the client secret is concealed,
// so we have to reset the primary client secret to ensure that user enters a new secret
credentials.clientSecret = undefined;
options = updateCredentials(options, credentials);
}
// Log Analytics credentials only used if primary credentials are App Registration (client secret)
if (credentials.authType === 'clientsecret') {
// Check whether the client secret is concealed
if (typeof credentials.clientSecret === 'symbol') {
// Log Analytics credentials need to be synchronized but the client secret is concealed,
// so we have to reset the primary client secret to ensure that user enters a new secret
credentials.clientSecret = undefined;
options = updateCredentials(options, credentials);
}
// Synchronize the Log Analytics credentials with primary credentials
options = updateLogAnalyticsCredentials(options, credentials);
// Synchronize the Log Analytics credentials with primary credentials
options = updateLogAnalyticsCredentials(options, credentials);
}
// Synchronize default subscription
options = {

View File

@@ -3,7 +3,13 @@ import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
import AppInsightsDatasource from './app_insights/app_insights_datasource';
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
import ResourcePickerData from './resourcePicker/resourcePickerData';
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, InsightsAnalyticsQuery } from './types';
import {
AzureDataSourceJsonData,
AzureMonitorQuery,
AzureQueryType,
DatasourceValidationResult,
InsightsAnalyticsQuery,
} from './types';
import {
DataFrame,
DataQueryRequest,
@@ -141,8 +147,8 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
return Promise.resolve([]);
}
async testDatasource() {
const promises: any[] = [];
async testDatasource(): Promise<DatasourceValidationResult> {
const promises: Array<Promise<DatasourceValidationResult>> = [];
if (this.azureMonitorDatasource.isConfigured()) {
promises.push(this.azureMonitorDatasource.testDatasource());
@@ -164,8 +170,8 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
};
}
return Promise.all(promises).then((results) => {
let status = 'success';
return await Promise.all(promises).then((results) => {
let status: 'success' | 'error' = 'success';
let message = '';
for (let i = 0; i < results.length; i++) {

View File

@@ -10,6 +10,12 @@ import Datasource from '../datasource';
export type AzureDataSourceSettings = DataSourceSettings<AzureDataSourceJsonData, AzureDataSourceSecureJsonData>;
export type AzureDataSourceInstanceSettings = DataSourceInstanceSettings<AzureDataSourceJsonData>;
export interface DatasourceValidationResult {
status: 'success' | 'error';
message: string;
title?: string;
}
export type AzureResultFormat = 'time_series' | 'table';
export enum AzureQueryType {
@@ -33,9 +39,29 @@ export interface AzureMonitorQuery extends DataQuery {
azureResourceGraph: AzureResourceGraphQuery;
}
/**
* Azure clouds known to Azure Monitor.
*/
export enum AzureCloud {
Public = 'AzureCloud',
China = 'AzureChinaCloud',
USGovernment = 'AzureUSGovernment',
Germany = 'AzureGermanCloud',
None = '',
}
export type AzureAuthType = 'msi' | 'clientsecret';
export type ConcealedSecret = symbol;
export interface AzureCredentials {
export type AzureCredentials = AzureManagedIdentityCredentials | AzureClientSecretCredentials;
export interface AzureManagedIdentityCredentials {
authType: 'msi';
}
export interface AzureClientSecretCredentials {
authType: 'clientsecret';
azureCloud?: string;
tenantId?: string;
clientId?: string;
@@ -44,6 +70,7 @@ export interface AzureCredentials {
export interface AzureDataSourceJsonData extends DataSourceJsonData {
cloudName: string;
azureAuthType?: AzureAuthType;
// monitor
tenantId?: string;