mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 08:52:14 +08:00
Azure: Unify credentials in frontend for MSSQL (#96357)
* init * test fix
This commit is contained in:
@ -1,92 +1,115 @@
|
||||
import { AzureAuthType, AzureCloud, AzureCredentialsType, ConcealedSecretType } from '../types';
|
||||
import {
|
||||
AzureCredentials,
|
||||
AzureCloud,
|
||||
ConcealedSecret,
|
||||
AzureClientSecretCredentials,
|
||||
instanceOfAzureCredential,
|
||||
updateDatasourceCredentials,
|
||||
} from '@grafana/azure-sdk';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
configWithManagedIdentityEnabled,
|
||||
configWithManagedIdentityDisabled,
|
||||
dataSourceSettingsWithMsiCredentials,
|
||||
dataSourceSettingsWithClientSecretOnServer,
|
||||
dataSourceSettingsWithClientSecretInSecureJSONData,
|
||||
} from './AzureAuth.testMocks';
|
||||
import { getDefaultCredentials, getSecret, getCredentials, updateCredentials } from './AzureCredentialsConfig';
|
||||
import { getDefaultCredentials, getCredentials } from './AzureCredentialsConfig';
|
||||
|
||||
// NOTE: @ts-ignores are used to ignore the type errors that are thrown when passing in the mocks.
|
||||
// This is because the mocks are partials of the actual types, so the types are not complete.
|
||||
|
||||
export const CLIENT_SECRET_SYMBOL: ConcealedSecretType = Symbol('Concealed client secret');
|
||||
export const CLIENT_SECRET_SYMBOL: ConcealedSecret = Symbol('Concealed client secret');
|
||||
|
||||
export const CLIENT_SECRET_STRING = 'XXXX-super-secret-secret-XXXX';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'), // Keep the rest of the actual module
|
||||
}));
|
||||
|
||||
describe('AzureAuth', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('AzureCredentialsConfig', () => {
|
||||
it('`getDefaultCredentials()` should return the correct credentials based on whether the managed identity is enabled', () => {
|
||||
const resultForManagedIdentityEnabled = getDefaultCredentials(true, AzureCloud.Public);
|
||||
const resultForManagedIdentityDisabled = getDefaultCredentials(false, AzureCloud.Public);
|
||||
jest.mocked(config).azure.managedIdentityEnabled = true;
|
||||
const resultForManagedIdentityEnabled = getDefaultCredentials();
|
||||
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
const resultForManagedIdentityDisabled = getDefaultCredentials();
|
||||
|
||||
expect(resultForManagedIdentityEnabled).toEqual({ authType: 'msi' });
|
||||
expect(resultForManagedIdentityDisabled).toEqual({ authType: 'clientsecret', azureCloud: 'AzureCloud' });
|
||||
});
|
||||
|
||||
it("`getSecret()` should correctly return the client secret if it's not concealed", () => {
|
||||
const resultFromServerSideSecret = getSecret(false, CLIENT_SECRET_STRING);
|
||||
expect(resultFromServerSideSecret).toBe(CLIENT_SECRET_STRING);
|
||||
|
||||
const resultFromSecureJSONDataSecret = typeof getSecret(true, '');
|
||||
expect(resultFromSecureJSONDataSecret).toBe('symbol');
|
||||
});
|
||||
|
||||
describe('getCredentials()', () => {
|
||||
it('should return the correct managed identity credentials', () => {
|
||||
// If `dataSourceSettings.authType === AzureAuthType.MSI` && `config.azure.managedIdentityEnabled === true`.
|
||||
// If `dataSourceSettings.authType === 'msi'` && `config.azure.managedIdentityEnabled === true`.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = true;
|
||||
const resultForManagedIdentityEnabled = getCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithMsiCredentials,
|
||||
configWithManagedIdentityEnabled
|
||||
dataSourceSettingsWithMsiCredentials
|
||||
);
|
||||
expect(resultForManagedIdentityEnabled).toEqual({ authType: AzureAuthType.MSI });
|
||||
expect(resultForManagedIdentityEnabled).toEqual({ authType: 'msi' });
|
||||
|
||||
// If `dataSourceSettings.authType === AzureAuthType.MSI` but `config.azure.managedIdentityEnabled !== true`.
|
||||
// If `dataSourceSettings.authType === 'msi'` but `config.azure.managedIdentityEnabled !== true`.
|
||||
// Default to basic client secret credentials.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
const resultForManagedIdentityEnabledInJSONButDisabledInConfig = getCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithMsiCredentials,
|
||||
configWithManagedIdentityDisabled
|
||||
dataSourceSettingsWithMsiCredentials
|
||||
);
|
||||
expect(resultForManagedIdentityEnabledInJSONButDisabledInConfig).toEqual({
|
||||
authType: AzureAuthType.CLIENT_SECRET,
|
||||
authType: 'clientsecret',
|
||||
azureCloud: 'AzureCloud',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the correct client secret credentials', () => {
|
||||
const basicExpectedResult = {
|
||||
authType: AzureAuthType.CLIENT_SECRET,
|
||||
authType: 'clientsecret',
|
||||
azureCloud: 'AzureCloud',
|
||||
tenantId: 'XXXX-tenant-id-XXXX',
|
||||
clientId: 'XXXX-client-id-XXXX',
|
||||
};
|
||||
|
||||
// If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == true`,
|
||||
// If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == true`,
|
||||
// i.e. the client secret is stored on the server.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
const resultForClientSecretCredentialsOnServer = getCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithClientSecretOnServer,
|
||||
configWithManagedIdentityDisabled
|
||||
dataSourceSettingsWithClientSecretOnServer
|
||||
);
|
||||
|
||||
// Here we test the properties separately because the client secret is a symbol,
|
||||
// and since JS symobls are unique, we test via the `typeof` operator.
|
||||
expect(resultForClientSecretCredentialsOnServer.authType).toEqual(AzureAuthType.CLIENT_SECRET);
|
||||
expect(resultForClientSecretCredentialsOnServer.azureCloud).toEqual('AzureCloud');
|
||||
expect(resultForClientSecretCredentialsOnServer.tenantId).toEqual('XXXX-tenant-id-XXXX');
|
||||
expect(resultForClientSecretCredentialsOnServer.clientId).toEqual('XXXX-client-id-XXXX');
|
||||
expect(typeof resultForClientSecretCredentialsOnServer.clientSecret).toEqual('symbol');
|
||||
expect(resultForClientSecretCredentialsOnServer.authType).toEqual('clientsecret');
|
||||
expect(
|
||||
instanceOfAzureCredential<AzureClientSecretCredentials>(
|
||||
'clientsecret',
|
||||
resultForClientSecretCredentialsOnServer
|
||||
)
|
||||
).toEqual(true);
|
||||
expect((resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).azureCloud).toEqual(
|
||||
'AzureCloud'
|
||||
);
|
||||
expect((resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).tenantId).toEqual(
|
||||
'XXXX-tenant-id-XXXX'
|
||||
);
|
||||
expect((resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).clientId).toEqual(
|
||||
'XXXX-client-id-XXXX'
|
||||
);
|
||||
expect(typeof (resultForClientSecretCredentialsOnServer as AzureClientSecretCredentials).clientSecret).toEqual(
|
||||
'symbol'
|
||||
);
|
||||
|
||||
// If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == false`,
|
||||
// If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == false`,
|
||||
// i.e. the client secret is stored in the secureJson.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
const resultForClientSecretCredentialsInSecureJSON = getCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithClientSecretInSecureJSONData,
|
||||
configWithManagedIdentityDisabled
|
||||
dataSourceSettingsWithClientSecretInSecureJSONData
|
||||
);
|
||||
expect(resultForClientSecretCredentialsInSecureJSON).toEqual({
|
||||
...basicExpectedResult,
|
||||
@ -97,66 +120,68 @@ describe('AzureAuth', () => {
|
||||
|
||||
describe('updateCredentials()', () => {
|
||||
it('should update the credentials for managed service identity correctly', () => {
|
||||
// If `dataSourceSettings.authType === AzureAuthType.MSI` && `config.azure.managedIdentityEnabled === true`.
|
||||
const resultForMsiCredentials = updateCredentials(
|
||||
// If `dataSourceSettings.authType === 'msi'` && `config.azure.managedIdentityEnabled === true`.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = true;
|
||||
const resultForMsiCredentials = updateDatasourceCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithMsiCredentials,
|
||||
configWithManagedIdentityEnabled,
|
||||
{
|
||||
authType: AzureAuthType.MSI,
|
||||
authType: 'msi',
|
||||
}
|
||||
);
|
||||
expect(resultForMsiCredentials).toEqual({ jsonData: { azureCredentials: { authType: 'msi' } } });
|
||||
|
||||
// If `dataSourceSettings.authType === AzureAuthType.MSI` but `config.azure.managedIdentityEnabled !== true`.
|
||||
// If `dataSourceSettings.authType === 'msi'` but `config.azure.managedIdentityEnabled !== true`.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
expect(() =>
|
||||
updateCredentials(
|
||||
updateDatasourceCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithMsiCredentials,
|
||||
configWithManagedIdentityDisabled,
|
||||
{
|
||||
authType: AzureAuthType.MSI,
|
||||
authType: 'msi',
|
||||
}
|
||||
)
|
||||
).toThrow('Managed Identity authentication is not enabled in Grafana config.');
|
||||
});
|
||||
|
||||
it('should update the credentials for client secret correctly', () => {
|
||||
const basicClientSecretCredentials: AzureCredentialsType = {
|
||||
authType: AzureAuthType.CLIENT_SECRET,
|
||||
azureCloud: 'AzureCloud',
|
||||
const basicClientSecretCredentials: AzureCredentials = {
|
||||
authType: 'clientsecret',
|
||||
azureCloud: AzureCloud.Public,
|
||||
tenantId: 'XXXX-tenant-id-XXXX',
|
||||
clientId: 'XXXX-client-id-XXXX',
|
||||
};
|
||||
|
||||
// If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == true`.
|
||||
const resultForClientSecretCredentials1 = updateCredentials(
|
||||
// If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == true`.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
const resultForClientSecretCredentials1 = updateDatasourceCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithClientSecretOnServer,
|
||||
configWithManagedIdentityDisabled,
|
||||
basicClientSecretCredentials
|
||||
);
|
||||
expect(resultForClientSecretCredentials1).toEqual({
|
||||
jsonData: {
|
||||
azureCredentials: { ...basicClientSecretCredentials },
|
||||
},
|
||||
secureJsonData: { azureClientSecret: undefined },
|
||||
secureJsonFields: { azureClientSecret: false },
|
||||
|
||||
expect(resultForClientSecretCredentials1.jsonData.azureCredentials).toEqual(basicClientSecretCredentials);
|
||||
expect(resultForClientSecretCredentials1.secureJsonData).toEqual({ azureClientSecret: undefined });
|
||||
expect(resultForClientSecretCredentials1.secureJsonFields).toEqual({
|
||||
azureClientSecret: false,
|
||||
clientSecret: false,
|
||||
});
|
||||
|
||||
// If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == false`.
|
||||
const resultForClientSecretCredentials2 = updateCredentials(
|
||||
// If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == false`.
|
||||
jest.mocked(config).azure.managedIdentityEnabled = false;
|
||||
const resultForClientSecretCredentials2 = updateDatasourceCredentials(
|
||||
// @ts-ignore
|
||||
dataSourceSettingsWithClientSecretInSecureJSONData,
|
||||
configWithManagedIdentityDisabled,
|
||||
{ ...basicClientSecretCredentials, clientSecret: 'XXXX-super-secret-secret-XXXX' }
|
||||
);
|
||||
expect(resultForClientSecretCredentials2).toEqual({
|
||||
jsonData: {
|
||||
azureCredentials: { ...basicClientSecretCredentials },
|
||||
},
|
||||
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' },
|
||||
secureJsonFields: { azureClientSecret: false },
|
||||
|
||||
expect(resultForClientSecretCredentials2.jsonData.azureCredentials).toEqual(basicClientSecretCredentials);
|
||||
expect(resultForClientSecretCredentials2.secureJsonData).toEqual({
|
||||
azureClientSecret: 'XXXX-super-secret-secret-XXXX',
|
||||
});
|
||||
expect(resultForClientSecretCredentials2.secureJsonFields).toEqual({
|
||||
azureClientSecret: false,
|
||||
clientSecret: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { AzureDataSourceSettings } from '@grafana/azure-sdk';
|
||||
import { GrafanaBootConfig } from '@grafana/runtime';
|
||||
|
||||
import { AzureAuthSecureJSONDataType, AzureAuthJSONDataType, AzureAuthType } from '../types';
|
||||
|
||||
export const configWithManagedIdentityEnabled: Partial<GrafanaBootConfig> = {
|
||||
azure: {
|
||||
managedIdentityEnabled: true,
|
||||
@ -24,31 +22,22 @@ export const configWithManagedIdentityDisabled: Partial<GrafanaBootConfig> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const dataSourceSettingsWithMsiCredentials: Partial<
|
||||
DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType>
|
||||
> = {
|
||||
jsonData: { azureCredentials: { authType: AzureAuthType.MSI } },
|
||||
};
|
||||
|
||||
const basicJSONData = {
|
||||
jsonData: {
|
||||
azureCredentials: {
|
||||
authType: AzureAuthType.CLIENT_SECRET,
|
||||
tenantId: 'XXXX-tenant-id-XXXX',
|
||||
clientId: 'XXXX-client-id-XXXX',
|
||||
},
|
||||
},
|
||||
export const dataSourceSettingsWithMsiCredentials: Partial<AzureDataSourceSettings> = {
|
||||
jsonData: { azureCredentials: { authType: 'msi' } },
|
||||
};
|
||||
|
||||
// Will return symbol as the secret is concealed
|
||||
export const dataSourceSettingsWithClientSecretOnServer: Partial<
|
||||
DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType>
|
||||
> = { ...basicJSONData, secureJsonFields: { azureClientSecret: true } };
|
||||
|
||||
// Will return the secret as a string from the secureJsonData
|
||||
export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial<
|
||||
DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType>
|
||||
> = {
|
||||
...basicJSONData,
|
||||
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX', password: undefined },
|
||||
export const dataSourceSettingsWithClientSecretOnServer: Partial<AzureDataSourceSettings> = {
|
||||
jsonData: {
|
||||
azureCredentials: { authType: 'clientsecret', clientId: 'XXXX-client-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX' },
|
||||
},
|
||||
secureJsonFields: { azureClientSecret: true },
|
||||
};
|
||||
// Will return the secret as a string from the secureJsonData
|
||||
export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial<AzureDataSourceSettings> = {
|
||||
jsonData: {
|
||||
azureCredentials: { authType: 'clientsecret', clientId: 'XXXX-client-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX' },
|
||||
},
|
||||
secureJsonFields: { azureClientSecret: false },
|
||||
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' },
|
||||
};
|
||||
|
@ -1,24 +1,25 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import { AzureCredentials, AzureCloud, updateDatasourceCredentials } from '@grafana/azure-sdk';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
|
||||
|
||||
import { AzureCredentialsType } from '../types';
|
||||
|
||||
import { KnownAzureClouds } from './AzureCredentials';
|
||||
import { getCredentials, updateCredentials } from './AzureCredentialsConfig';
|
||||
import { getCredentials } from './AzureCredentialsConfig';
|
||||
import { AzureCredentialsForm } from './AzureCredentialsForm';
|
||||
|
||||
export const KnownAzureClouds: Array<SelectableValue<AzureCloud>> = [{ value: AzureCloud.Public, label: 'Azure' }];
|
||||
|
||||
export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
|
||||
const { dataSourceConfig: dsSettings, onChange } = props;
|
||||
const managedIdentityEnabled = config.azure.managedIdentityEnabled;
|
||||
const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled;
|
||||
|
||||
const credentials = useMemo(() => getCredentials(dsSettings, config), [dsSettings]);
|
||||
const credentials = useMemo(() => getCredentials(dsSettings), [dsSettings]);
|
||||
|
||||
const onCredentialsChange = (credentials: AzureCredentialsType): void => {
|
||||
onChange(updateCredentials(dsSettings, config, credentials));
|
||||
const onCredentialsChange = (credentials: AzureCredentials): void => {
|
||||
onChange(updateDatasourceCredentials(dsSettings, credentials));
|
||||
};
|
||||
|
||||
// The auth type needs to be set on the first load of the data source
|
||||
|
@ -1,21 +0,0 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
import { AzureCredentialsType, AzureAuthType } from '../types';
|
||||
|
||||
export enum AzureCloud {
|
||||
Public = 'AzureCloud',
|
||||
None = '',
|
||||
}
|
||||
|
||||
export const KnownAzureClouds: Array<SelectableValue<AzureCloud>> = [{ value: AzureCloud.Public, label: 'Azure' }];
|
||||
|
||||
export function isCredentialsComplete(credentials: AzureCredentialsType): boolean {
|
||||
switch (credentials.authType) {
|
||||
case AzureAuthType.MSI:
|
||||
return true;
|
||||
case AzureAuthType.CLIENT_SECRET:
|
||||
return !!(credentials.azureCloud && credentials.tenantId && credentials.clientId && credentials.clientSecret);
|
||||
case AzureAuthType.AD_PASSWORD:
|
||||
return !!(credentials.clientId && credentials.password && credentials.userId);
|
||||
}
|
||||
}
|
@ -1,167 +1,26 @@
|
||||
import { DataSourceSettings } from '@grafana/data';
|
||||
import { GrafanaBootConfig } from '@grafana/runtime';
|
||||
|
||||
import {
|
||||
AzureCloud,
|
||||
AzureCredentialsType,
|
||||
ConcealedSecretType,
|
||||
AzureAuthSecureJSONDataType,
|
||||
AzureAuthJSONDataType,
|
||||
AzureAuthType,
|
||||
} from '../types';
|
||||
AzureCredentials,
|
||||
AzureDataSourceSettings,
|
||||
getDatasourceCredentials,
|
||||
getDefaultAzureCloud,
|
||||
} from '@grafana/azure-sdk';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
export const getDefaultCredentials = (managedIdentityEnabled: boolean, cloud: string): AzureCredentialsType => {
|
||||
if (managedIdentityEnabled) {
|
||||
return { authType: AzureAuthType.MSI };
|
||||
export const getDefaultCredentials = (): AzureCredentials => {
|
||||
if (config.azure.managedIdentityEnabled) {
|
||||
return { authType: 'msi' };
|
||||
} else {
|
||||
return { authType: AzureAuthType.CLIENT_SECRET, azureCloud: cloud };
|
||||
return { authType: 'clientsecret', azureCloud: getDefaultAzureCloud() };
|
||||
}
|
||||
};
|
||||
|
||||
export const getSecret = (
|
||||
storedServerSide: boolean,
|
||||
secret: string | symbol | undefined
|
||||
): undefined | string | ConcealedSecretType => {
|
||||
const concealedSecret: ConcealedSecretType = Symbol('Concealed client secret');
|
||||
if (storedServerSide) {
|
||||
// The secret is concealed server side, so return the symbol
|
||||
return concealedSecret;
|
||||
} else {
|
||||
return typeof secret === 'string' && secret.length > 0 ? secret : undefined;
|
||||
export const getCredentials = (dsSettings: AzureDataSourceSettings): AzureCredentials => {
|
||||
const credentials = getDatasourceCredentials(dsSettings);
|
||||
if (credentials) {
|
||||
return credentials;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCredentials = (
|
||||
dsSettings: DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType>,
|
||||
bootConfig: GrafanaBootConfig
|
||||
): AzureCredentialsType => {
|
||||
// JSON data
|
||||
const credentials = dsSettings.jsonData?.azureCredentials;
|
||||
|
||||
// Secure JSON data/fields
|
||||
const clientSecretStoredServerSide = dsSettings.secureJsonFields?.azureClientSecret;
|
||||
const clientSecret = dsSettings.secureJsonData?.azureClientSecret;
|
||||
const passwordStoredServerSide = dsSettings.secureJsonFields?.password;
|
||||
const password = dsSettings.secureJsonData?.password;
|
||||
|
||||
// BootConfig data
|
||||
const managedIdentityEnabled = !!bootConfig.azure?.managedIdentityEnabled;
|
||||
const cloud = bootConfig.azure?.cloud || AzureCloud.Public;
|
||||
|
||||
// If no credentials saved, then return empty credentials
|
||||
// of type based on whether the managed identity enabled
|
||||
if (!credentials) {
|
||||
return getDefaultCredentials(managedIdentityEnabled, cloud);
|
||||
}
|
||||
|
||||
switch (credentials.authType) {
|
||||
case AzureAuthType.MSI:
|
||||
if (managedIdentityEnabled) {
|
||||
return {
|
||||
authType: AzureAuthType.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: AzureAuthType.CLIENT_SECRET,
|
||||
azureCloud: cloud,
|
||||
};
|
||||
}
|
||||
case AzureAuthType.CLIENT_SECRET:
|
||||
return {
|
||||
authType: AzureAuthType.CLIENT_SECRET,
|
||||
azureCloud: credentials.azureCloud || cloud,
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
clientSecret: getSecret(clientSecretStoredServerSide, clientSecret),
|
||||
};
|
||||
case AzureAuthType.AD_PASSWORD:
|
||||
return {
|
||||
authType: AzureAuthType.AD_PASSWORD,
|
||||
userId: credentials.userId,
|
||||
clientId: credentials.clientId,
|
||||
password: getSecret(passwordStoredServerSide, password),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const updateCredentials = (
|
||||
dsSettings: DataSourceSettings<AzureAuthJSONDataType>,
|
||||
bootConfig: GrafanaBootConfig,
|
||||
credentials: AzureCredentialsType
|
||||
): DataSourceSettings<AzureAuthJSONDataType> => {
|
||||
// BootConfig data
|
||||
const managedIdentityEnabled = !!bootConfig.azure?.managedIdentityEnabled;
|
||||
const cloud = bootConfig.azure?.cloud || AzureCloud.Public;
|
||||
|
||||
switch (credentials.authType) {
|
||||
case AzureAuthType.MSI:
|
||||
if (!managedIdentityEnabled) {
|
||||
throw new Error('Managed Identity authentication is not enabled in Grafana config.');
|
||||
}
|
||||
|
||||
dsSettings = {
|
||||
...dsSettings,
|
||||
jsonData: {
|
||||
...dsSettings.jsonData,
|
||||
azureCredentials: {
|
||||
authType: AzureAuthType.MSI,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return dsSettings;
|
||||
|
||||
case AzureAuthType.CLIENT_SECRET:
|
||||
dsSettings = {
|
||||
...dsSettings,
|
||||
jsonData: {
|
||||
...dsSettings.jsonData,
|
||||
azureCredentials: {
|
||||
authType: AzureAuthType.CLIENT_SECRET,
|
||||
azureCloud: credentials.azureCloud || cloud,
|
||||
tenantId: credentials.tenantId,
|
||||
clientId: credentials.clientId,
|
||||
},
|
||||
},
|
||||
secureJsonData: {
|
||||
...dsSettings.secureJsonData,
|
||||
azureClientSecret:
|
||||
typeof credentials.clientSecret === 'string' && credentials.clientSecret.length > 0
|
||||
? credentials.clientSecret
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...dsSettings.secureJsonFields,
|
||||
azureClientSecret: typeof credentials.clientSecret === 'symbol',
|
||||
},
|
||||
};
|
||||
|
||||
return dsSettings;
|
||||
|
||||
case AzureAuthType.AD_PASSWORD:
|
||||
return {
|
||||
...dsSettings,
|
||||
jsonData: {
|
||||
...dsSettings.jsonData,
|
||||
azureCredentials: {
|
||||
authType: AzureAuthType.AD_PASSWORD,
|
||||
userId: credentials.userId,
|
||||
clientId: credentials.clientId,
|
||||
},
|
||||
},
|
||||
secureJsonData: {
|
||||
...dsSettings.secureJsonData,
|
||||
password:
|
||||
typeof credentials.password === 'string' && credentials.password.length > 0
|
||||
? credentials.password
|
||||
: undefined,
|
||||
},
|
||||
secureJsonFields: {
|
||||
...dsSettings.secureJsonFields,
|
||||
password: typeof credentials.password === 'symbol',
|
||||
},
|
||||
};
|
||||
}
|
||||
return getDefaultCredentials();
|
||||
};
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import { AzureCredentials, AzureAuthType } from '@grafana/azure-sdk';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Button, Field, Select, Input } from '@grafana/ui/src/components';
|
||||
|
||||
import { AzureCredentialsType, AzureAuthType } from '../types';
|
||||
|
||||
export interface Props {
|
||||
managedIdentityEnabled: boolean;
|
||||
azureEntraPasswordCredentialsEnabled: boolean;
|
||||
credentials: AzureCredentialsType;
|
||||
credentials: AzureCredentials;
|
||||
azureCloudOptions?: SelectableValue[];
|
||||
onCredentialsChange: (updatedCredentials: AzureCredentialsType) => void;
|
||||
onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
@ -26,9 +25,89 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
|
||||
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
|
||||
if (onCredentialsChange) {
|
||||
const updated: AzureCredentialsType = {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
authType: selected.value || AzureAuthType.MSI,
|
||||
authType: selected.value || 'msi',
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onAzureCloudChange = (selected: SelectableValue<string>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
azureCloud: selected.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
tenantId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret' || credentials.authType === 'ad-password') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onClientSecretReset = () => {
|
||||
if (credentials.authType === 'clientsecret') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
clientSecret: '',
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onUserIdChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'ad-password') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
userId: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
if (credentials.authType === 'ad-password') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
password: event.target.value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
const onPasswordReset = () => {
|
||||
if (credentials.authType === 'ad-password') {
|
||||
const updated: AzureCredentials = {
|
||||
...credentials,
|
||||
password: '',
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
@ -36,33 +115,23 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
|
||||
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
|
||||
{
|
||||
value: AzureAuthType.CLIENT_SECRET,
|
||||
value: 'clientsecret',
|
||||
label: 'App Registration',
|
||||
},
|
||||
];
|
||||
if (managedIdentityEnabled) {
|
||||
authTypeOptions.push({
|
||||
value: AzureAuthType.MSI,
|
||||
value: 'msi',
|
||||
label: 'Managed Identity',
|
||||
});
|
||||
}
|
||||
if (azureEntraPasswordCredentialsEnabled) {
|
||||
authTypeOptions.push({
|
||||
value: AzureAuthType.AD_PASSWORD,
|
||||
value: 'ad-password',
|
||||
label: 'Azure Entra Password',
|
||||
});
|
||||
}
|
||||
|
||||
const onInputChange = ({ property, value }: { property: keyof AzureCredentialsType; value: string }) => {
|
||||
if (onCredentialsChange) {
|
||||
const updated: AzureCredentialsType = {
|
||||
...credentials,
|
||||
[property]: value,
|
||||
};
|
||||
onCredentialsChange(updated);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Field
|
||||
@ -78,17 +147,14 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Field>
|
||||
{credentials.authType === AzureAuthType.CLIENT_SECRET && (
|
||||
{credentials.authType === 'clientsecret' && (
|
||||
<>
|
||||
{azureCloudOptions && (
|
||||
<Field label="Azure Cloud" htmlFor="azure-cloud-type" disabled={disabled}>
|
||||
<Select
|
||||
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
|
||||
options={azureCloudOptions}
|
||||
onChange={(selected: SelectableValue<AzureAuthType>) => {
|
||||
const value = selected.value || '';
|
||||
onInputChange({ property: 'azureCloud', value });
|
||||
}}
|
||||
onChange={onAzureCloudChange}
|
||||
isDisabled={disabled}
|
||||
inputId="azure-cloud-type"
|
||||
aria-label="Azure Cloud"
|
||||
@ -107,10 +173,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
width={45}
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.tenantId || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onInputChange({ property: 'tenantId', value });
|
||||
}}
|
||||
onChange={onTenantIdChange}
|
||||
disabled={disabled}
|
||||
aria-label="Tenant ID"
|
||||
/>
|
||||
@ -126,10 +189,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
width={45}
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientId || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onInputChange({ property: 'clientId', value });
|
||||
}}
|
||||
onChange={onClientIdChange}
|
||||
disabled={disabled}
|
||||
aria-label="Client ID"
|
||||
/>
|
||||
@ -145,14 +205,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
data-testid={'client-secret'}
|
||||
width={45}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onInputChange({ property: 'clientSecret', value: '' });
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button variant="secondary" type="button" onClick={onClientSecretReset} disabled={disabled}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
@ -170,10 +223,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
aria-label="Client Secret"
|
||||
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
|
||||
value={credentials.clientSecret || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onInputChange({ property: 'clientSecret', value });
|
||||
}}
|
||||
onChange={onClientSecretChange}
|
||||
id="client-secret"
|
||||
disabled={disabled}
|
||||
/>
|
||||
@ -181,16 +231,13 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{credentials.authType === AzureAuthType.AD_PASSWORD && azureEntraPasswordCredentialsEnabled && (
|
||||
{credentials.authType === 'ad-password' && azureEntraPasswordCredentialsEnabled && (
|
||||
<>
|
||||
<Field label="User Id" required htmlFor="user-id" invalid={!credentials.userId} error={'User ID is required'}>
|
||||
<Input
|
||||
width={45}
|
||||
value={credentials.userId || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onInputChange({ property: 'userId', value });
|
||||
}}
|
||||
onChange={onUserIdChange}
|
||||
disabled={disabled}
|
||||
aria-label="User ID"
|
||||
/>
|
||||
@ -205,10 +252,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
<Input
|
||||
width={45}
|
||||
value={credentials.clientId || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onInputChange({ property: 'clientId', value });
|
||||
}}
|
||||
onChange={onClientIdChange}
|
||||
disabled={disabled}
|
||||
aria-label="Application Client ID"
|
||||
/>
|
||||
@ -224,14 +268,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
data-testid={'password'}
|
||||
width={45}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onInputChange({ property: 'password', value: '' });
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Button variant="secondary" type="button" onClick={onPasswordReset} disabled={disabled}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
@ -248,10 +285,7 @@ export const AzureCredentialsForm = (props: Props) => {
|
||||
width={45}
|
||||
aria-label="Password"
|
||||
value={credentials.password || ''}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value;
|
||||
onInputChange({ property: 'password', value });
|
||||
}}
|
||||
onChange={onPasswordChange}
|
||||
id="password"
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DataSourceJsonData } from '@grafana/data';
|
||||
import { AzureCredentials } from '@grafana/azure-sdk';
|
||||
import { SQLOptions } from '@grafana/sql';
|
||||
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
|
||||
|
||||
@ -17,37 +17,13 @@ export enum MSSQLEncryptOptions {
|
||||
false = 'false',
|
||||
true = 'true',
|
||||
}
|
||||
|
||||
export enum AzureCloud {
|
||||
Public = 'AzureCloud',
|
||||
None = '',
|
||||
}
|
||||
|
||||
export type ConcealedSecretType = symbol;
|
||||
|
||||
export enum AzureAuthType {
|
||||
MSI = 'msi',
|
||||
CLIENT_SECRET = 'clientsecret',
|
||||
AD_PASSWORD = 'ad-password',
|
||||
}
|
||||
|
||||
export interface AzureCredentialsType {
|
||||
authType: AzureAuthType;
|
||||
azureCloud?: string;
|
||||
tenantId?: string;
|
||||
clientId?: string;
|
||||
clientSecret?: string | ConcealedSecretType;
|
||||
userId?: string;
|
||||
password?: string | ConcealedSecretType;
|
||||
}
|
||||
|
||||
export interface MssqlOptions extends SQLOptions {
|
||||
authenticationType?: MSSQLAuthenticationType;
|
||||
encrypt?: MSSQLEncryptOptions;
|
||||
sslRootCertFile?: string;
|
||||
serverName?: string;
|
||||
connectionTimeout?: number;
|
||||
azureCredentials?: AzureCredentialsType;
|
||||
azureCredentials?: AzureCredentials;
|
||||
keytabFilePath?: string;
|
||||
credentialCache?: string;
|
||||
credentialCacheLookupFile?: string;
|
||||
@ -60,15 +36,6 @@ export interface MssqlSecureOptions {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export type AzureAuthJSONDataType = DataSourceJsonData & {
|
||||
azureCredentials: AzureCredentialsType;
|
||||
};
|
||||
|
||||
export type AzureAuthSecureJSONDataType = {
|
||||
azureClientSecret: undefined | string | ConcealedSecretType;
|
||||
password: undefined | string | ConcealedSecretType;
|
||||
};
|
||||
|
||||
export type AzureAuthConfigType = {
|
||||
azureAuthIsSupported: boolean;
|
||||
azureAuthSettingsUI: (props: HttpSettingsBaseProps) => JSX.Element;
|
||||
|
Reference in New Issue
Block a user