mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
parent
2303694293
commit
148289258f
@ -2,11 +2,11 @@ import { cx } from '@emotion/css';
|
||||
import { FormEvent, useMemo, useState } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import { AzureCredentials } from '@grafana/azure-sdk';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, InlineSwitch, Input } from '@grafana/ui';
|
||||
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
|
||||
|
||||
import { AzureCredentials } from './AzureCredentials';
|
||||
import { getAzureCloudOptions, getCredentials, updateCredentials } from './AzureCredentialsConfig';
|
||||
import { AzureCredentialsForm } from './AzureCredentialsForm';
|
||||
|
||||
|
@ -1,46 +0,0 @@
|
||||
export enum AzureCloud {
|
||||
Public = 'AzureCloud',
|
||||
China = 'AzureChinaCloud',
|
||||
USGovernment = 'AzureUSGovernment',
|
||||
None = '',
|
||||
}
|
||||
|
||||
export type AzureAuthType = 'msi' | 'clientsecret' | 'workloadidentity';
|
||||
|
||||
export type ConcealedSecret = symbol;
|
||||
|
||||
interface AzureCredentialsBase {
|
||||
authType: AzureAuthType;
|
||||
defaultSubscriptionId?: string;
|
||||
}
|
||||
|
||||
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 type AzureCredentials =
|
||||
| AzureManagedIdentityCredentials
|
||||
| AzureClientSecretCredentials
|
||||
| AzureWorkloadIdentityCredentials;
|
||||
|
||||
export function isCredentialsComplete(credentials: AzureCredentials): boolean {
|
||||
switch (credentials.authType) {
|
||||
case 'msi':
|
||||
case 'workloadidentity':
|
||||
return true;
|
||||
case 'clientsecret':
|
||||
return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret);
|
||||
}
|
||||
}
|
@ -1,29 +1,17 @@
|
||||
import { getAzureClouds } from '@grafana/azure-sdk';
|
||||
import {
|
||||
AzureCredentials,
|
||||
AzureDataSourceJsonData,
|
||||
AzureDataSourceSecureJsonData,
|
||||
AzureDataSourceSettings,
|
||||
getAzureClouds,
|
||||
getDatasourceCredentials,
|
||||
getDefaultAzureCloud,
|
||||
updateDatasourceCredentials,
|
||||
} from '@grafana/azure-sdk';
|
||||
import { DataSourceSettings, SelectableValue } from '@grafana/data';
|
||||
import { PromOptions } from '@grafana/prometheus';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { AzureCloud, AzureCredentials, ConcealedSecret } from './AzureCredentials';
|
||||
|
||||
const concealed: ConcealedSecret = Symbol('Concealed client secret');
|
||||
|
||||
function getDefaultAzureCloud(): string {
|
||||
return config.azure.cloud || AzureCloud.Public;
|
||||
}
|
||||
|
||||
function getSecret(options: DataSourceSettings<any, any>): undefined | string | ConcealedSecret {
|
||||
if (options.secureJsonFields.azureClientSecret) {
|
||||
// The secret is concealed on server
|
||||
return concealed;
|
||||
} else {
|
||||
const secret = options.secureJsonData?.azureClientSecret;
|
||||
return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasCredentials(options: DataSourceSettings<any, any>): boolean {
|
||||
return !!options.jsonData.azureCredentials;
|
||||
}
|
||||
|
||||
export function getAzureCloudOptions(): Array<SelectableValue<string>> {
|
||||
const cloudInfo = getAzureClouds();
|
||||
|
||||
@ -41,101 +29,25 @@ export function getDefaultCredentials(): AzureCredentials {
|
||||
}
|
||||
}
|
||||
|
||||
export function getCredentials(options: DataSourceSettings<any, any>): AzureCredentials {
|
||||
const credentials = options.jsonData.azureCredentials as AzureCredentials | undefined;
|
||||
export function getCredentials(options: AzureDataSourceSettings): AzureCredentials {
|
||||
const credentials = getDatasourceCredentials(options);
|
||||
if (credentials) {
|
||||
return credentials;
|
||||
}
|
||||
|
||||
// If no credentials saved, then return empty credentials
|
||||
// of type based on whether the managed identity enabled
|
||||
if (!credentials) {
|
||||
return getDefaultCredentials();
|
||||
}
|
||||
|
||||
switch (credentials.authType) {
|
||||
case 'msi':
|
||||
case 'workloadidentity':
|
||||
if (
|
||||
(credentials.authType === 'msi' && config.azure.managedIdentityEnabled) ||
|
||||
(credentials.authType === 'workloadidentity' && config.azure.workloadIdentityEnabled)
|
||||
) {
|
||||
return {
|
||||
authType: credentials.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: 'clientsecret',
|
||||
azureCloud: credentials.azureCloud || getDefaultAzureCloud(),
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
clientSecret: getSecret(options),
|
||||
};
|
||||
}
|
||||
return getDefaultCredentials();
|
||||
}
|
||||
|
||||
export function updateCredentials(
|
||||
options: DataSourceSettings<any, any>,
|
||||
options: AzurePromDataSourceSettings,
|
||||
credentials: AzureCredentials
|
||||
): DataSourceSettings<any, any> {
|
||||
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,
|
||||
azureCredentials: {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: credentials.azureCloud || getDefaultAzureCloud(),
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
},
|
||||
},
|
||||
secureJsonData: {
|
||||
...options.secureJsonData,
|
||||
azureClientSecret:
|
||||
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
|
||||
? credentials.clientSecret
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...options.secureJsonFields,
|
||||
azureClientSecret: typeof credentials.clientSecret === 'symbol',
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
): AzurePromDataSourceSettings {
|
||||
return updateDatasourceCredentials(options, credentials);
|
||||
}
|
||||
|
||||
export function setDefaultCredentials(options: DataSourceSettings<any, any>): Partial<DataSourceSettings<any, any>> {
|
||||
export function setDefaultCredentials(options: AzurePromDataSourceSettings): Partial<AzurePromDataSourceSettings> {
|
||||
return {
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
@ -144,13 +56,18 @@ export function setDefaultCredentials(options: DataSourceSettings<any, any>): Pa
|
||||
};
|
||||
}
|
||||
|
||||
export function resetCredentials(options: DataSourceSettings<any, any>): Partial<DataSourceSettings<any, any>> {
|
||||
export function resetCredentials(options: AzurePromDataSourceSettings): Partial<AzurePromDataSourceSettings> {
|
||||
return {
|
||||
jsonData: {
|
||||
...options.jsonData,
|
||||
azureAuth: undefined,
|
||||
azureCredentials: undefined,
|
||||
azureEndpointResourceId: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface AzurePromDataSourceOptions extends PromOptions, AzureDataSourceJsonData {
|
||||
azureEndpointResourceId?: string;
|
||||
}
|
||||
|
||||
export type AzurePromDataSourceSettings = DataSourceSettings<AzurePromDataSourceOptions, AzureDataSourceSecureJsonData>;
|
||||
|
@ -12,7 +12,6 @@ const setup = (propsFunc?: (props: Props) => Props) => {
|
||||
tenantId: 'e7f3f661-a933-3h3f-0294-31c4f962ec48',
|
||||
clientId: '34509fad-c0r9-45df-9e25-f1ee34af6900',
|
||||
clientSecret: undefined,
|
||||
defaultSubscriptionId: '44987801-6nn6-49he-9b2d-9106972f9789',
|
||||
},
|
||||
azureCloudOptions: [
|
||||
{ value: 'azuremonitor', label: 'Azure' },
|
||||
@ -45,15 +44,4 @@ describe('AzureCredentialsForm', () => {
|
||||
}));
|
||||
expect(await screen.findByLabelText('Client Secret')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should enable azure monitor load subscriptions button when all required fields are defined', async () => {
|
||||
setup((props) => ({
|
||||
...props,
|
||||
credentials: {
|
||||
...props.credentials,
|
||||
clientSecret: 'e7f3f661-a933-4b3f-8176-51c4f982ec48',
|
||||
},
|
||||
}));
|
||||
expect(await screen.findByRole('button', { name: 'Load Subscriptions' })).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { ChangeEvent, useEffect, useMemo, useReducer, useState } from 'react';
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
|
||||
import { AzureAuthType, AzureCredentials } from '@grafana/azure-sdk';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { InlineFormLabel, Button, Select, Input } from '@grafana/ui';
|
||||
|
||||
import { AzureAuthType, AzureCredentials, isCredentialsComplete } from './AzureCredentials';
|
||||
|
||||
export interface Props {
|
||||
managedIdentityEnabled: boolean;
|
||||
workloadIdentityEnabled: boolean;
|
||||
@ -22,15 +21,10 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
credentials,
|
||||
azureCloudOptions,
|
||||
onCredentialsChange,
|
||||
getSubscriptions,
|
||||
disabled,
|
||||
managedIdentityEnabled,
|
||||
workloadIdentityEnabled,
|
||||
} = props;
|
||||
const hasRequiredFields = isCredentialsComplete(credentials);
|
||||
|
||||
const [subscriptions, setSubscriptions] = useState<Array<SelectableValue<string>>>([]);
|
||||
const [loadSubscriptionsClicked, onLoadSubscriptions] = useReducer((val) => val + 1, 0);
|
||||
|
||||
const authTypeOptions = useMemo(() => {
|
||||
let opts: Array<SelectableValue<AzureAuthType>> = [
|
||||
@ -56,42 +50,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
return opts;
|
||||
}, [managedIdentityEnabled, workloadIdentityEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!getSubscriptions || !hasRequiredFields) {
|
||||
updateSubscriptions([]);
|
||||
return;
|
||||
}
|
||||
let canceled = false;
|
||||
getSubscriptions().then((result) => {
|
||||
if (!canceled) {
|
||||
updateSubscriptions(result, loadSubscriptionsClicked);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
// This effect is intended to be called only once initially and on Load Subscriptions click
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loadSubscriptionsClicked]);
|
||||
|
||||
const updateSubscriptions = (received: Array<SelectableValue<string>>, autoSelect = false) => {
|
||||
setSubscriptions(received);
|
||||
if (getSubscriptions) {
|
||||
if (autoSelect && !credentials.defaultSubscriptionId && received.length > 0) {
|
||||
// Selecting the default subscription if subscriptions received but no default subscription selected
|
||||
onSubscriptionChange(received[0]);
|
||||
} else if (credentials.defaultSubscriptionId) {
|
||||
const found = received.find((opt) => opt.value === credentials.defaultSubscriptionId);
|
||||
if (!found) {
|
||||
// Unselecting the default subscription if it isn't found among the received subscriptions
|
||||
onSubscriptionChange(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
|
||||
setSubscriptions([]);
|
||||
const defaultAuthType = managedIdentityEnabled
|
||||
? 'msi'
|
||||
: workloadIdentityEnabled
|
||||
@ -100,18 +59,15 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
authType: selected.value || defaultAuthType,
|
||||
defaultSubscriptionId: undefined,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
|
||||
const onAzureCloudChange = (selected: SelectableValue<string>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
setSubscriptions([]);
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
azureCloud: selected.value,
|
||||
defaultSubscriptionId: undefined,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
@ -119,11 +75,9 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
|
||||
const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
setSubscriptions([]);
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
tenantId: event.target.value,
|
||||
defaultSubscriptionId: undefined,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
@ -131,11 +85,9 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
|
||||
const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
setSubscriptions([]);
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientId: event.target.value,
|
||||
defaultSubscriptionId: undefined,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
@ -143,11 +95,9 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
|
||||
const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
setSubscriptions([]);
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: event.target.value,
|
||||
defaultSubscriptionId: undefined,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
@ -155,23 +105,14 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
|
||||
const onClientSecretReset = () => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
setSubscriptions([]);
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: '',
|
||||
defaultSubscriptionId: undefined,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubscriptionChange = (selected: SelectableValue<string> | undefined) => {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
defaultSubscriptionId: selected?.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
};
|
||||
const prometheusConfigOverhaulAuth = config.featureToggles.prometheusConfigOverhaulAuth;
|
||||
|
||||
return (
|
||||
@ -283,42 +224,6 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{getSubscriptions && (
|
||||
<>
|
||||
<div className="gf-form-inline">
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel className="width-12">Default Subscription</InlineFormLabel>
|
||||
<div className={cx(prometheusConfigOverhaulAuth ? 'width-20' : 'width-25')}>
|
||||
<Select
|
||||
value={
|
||||
credentials.defaultSubscriptionId
|
||||
? subscriptions.find((opt) => opt.value === credentials.defaultSubscriptionId)
|
||||
: undefined
|
||||
}
|
||||
options={subscriptions}
|
||||
onChange={onSubscriptionChange}
|
||||
isDisabled={disabled}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { SIGV4ConnectionConfig } from '@grafana/aws-sdk';
|
||||
import { DataSourcePluginOptionsEditorProps, DataSourceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { hasCredentials } from '@grafana/azure-sdk';
|
||||
import { DataSourcePluginOptionsEditorProps, GrafanaTheme2 } from '@grafana/data';
|
||||
import { AdvancedHttpSettings, ConfigSection, DataSourceDescription } from '@grafana/experimental';
|
||||
import { AlertingSettingsOverhaul, PromOptions, PromSettings } from '@grafana/prometheus';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Alert, FieldValidationMessage, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { AzureAuthSettings } from './AzureAuthSettings';
|
||||
import { hasCredentials, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
|
||||
import { AzurePromDataSourceSettings, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
|
||||
import { DataSourcehttpSettingsOverhaul } from './DataSourceHttpSettingsOverhaulPackage';
|
||||
|
||||
export const PROM_CONFIG_LABEL_WIDTH = 30;
|
||||
@ -20,8 +21,8 @@ export const ConfigEditor = (props: Props) => {
|
||||
|
||||
const azureAuthSettings = {
|
||||
azureAuthSupported: config.azureAuthEnabled,
|
||||
getAzureAuthEnabled: (config: DataSourceSettings): boolean => hasCredentials(config),
|
||||
setAzureAuthEnabled: (config: DataSourceSettings, enabled: boolean) =>
|
||||
getAzureAuthEnabled: (config: AzurePromDataSourceSettings): boolean => hasCredentials(config),
|
||||
setAzureAuthEnabled: (config: AzurePromDataSourceSettings, enabled: boolean) =>
|
||||
enabled ? setDefaultCredentials(config) : resetCredentials(config),
|
||||
azureSettingsUI: AzureAuthSettings,
|
||||
};
|
||||
|
@ -1,22 +1,17 @@
|
||||
import { ReactElement, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { Auth, ConnectionSettings, convertLegacyAuthProps, AuthMethod } from '@grafana/experimental';
|
||||
import { PromOptions, docsTip, overhaulStyles } from '@grafana/prometheus';
|
||||
import { docsTip, overhaulStyles } from '@grafana/prometheus';
|
||||
import { Alert, SecureSocksProxySettings, useTheme2 } from '@grafana/ui';
|
||||
// NEED TO EXPORT THIS FROM GRAFANA/UI FOR EXTERNAL DS
|
||||
import { AzureAuthSettings } from '@grafana/ui/src/components/DataSourceSettings/types';
|
||||
|
||||
import type { AzureCredentials } from './AzureCredentials';
|
||||
|
||||
interface PromOptionsWithCloudAuth extends PromOptions {
|
||||
azureCredentials?: AzureCredentials;
|
||||
}
|
||||
import { AzurePromDataSourceSettings } from './AzureCredentialsConfig';
|
||||
|
||||
type Props = {
|
||||
options: DataSourceSettings<PromOptionsWithCloudAuth, {}>;
|
||||
onOptionsChange: (options: DataSourceSettings<PromOptionsWithCloudAuth, {}>) => void;
|
||||
options: AzurePromDataSourceSettings;
|
||||
onOptionsChange: (options: AzurePromDataSourceSettings) => void;
|
||||
azureAuthSettings: AzureAuthSettings;
|
||||
sigV4AuthToggleEnabled: boolean | undefined;
|
||||
renderSigV4Editor: React.ReactNode;
|
||||
|
Loading…
Reference in New Issue
Block a user