Azure: Unify credentials in frontend for MSSQL (#96357)

* init

* test fix
pull/97643/head^2
Younjin Song 7 months ago committed by GitHub
parent 663167a16c
commit 85392de2e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 153
      public/app/plugins/datasource/mssql/azureauth/AzureAuth.test.ts
  2. 37
      public/app/plugins/datasource/mssql/azureauth/AzureAuth.testMocks.ts
  3. 15
      public/app/plugins/datasource/mssql/azureauth/AzureAuthSettings.tsx
  4. 21
      public/app/plugins/datasource/mssql/azureauth/AzureCredentials.ts
  5. 173
      public/app/plugins/datasource/mssql/azureauth/AzureCredentialsConfig.ts
  6. 164
      public/app/plugins/datasource/mssql/azureauth/AzureCredentialsForm.tsx
  7. 37
      public/app/plugins/datasource/mssql/types.ts

@ -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 { import {
configWithManagedIdentityEnabled,
configWithManagedIdentityDisabled,
dataSourceSettingsWithMsiCredentials, dataSourceSettingsWithMsiCredentials,
dataSourceSettingsWithClientSecretOnServer, dataSourceSettingsWithClientSecretOnServer,
dataSourceSettingsWithClientSecretInSecureJSONData, dataSourceSettingsWithClientSecretInSecureJSONData,
} from './AzureAuth.testMocks'; } 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. // 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. // 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'; 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', () => { describe('AzureAuth', () => {
beforeEach(() => {
jest.resetModules();
});
describe('AzureCredentialsConfig', () => { describe('AzureCredentialsConfig', () => {
it('`getDefaultCredentials()` should return the correct credentials based on whether the managed identity is enabled', () => { it('`getDefaultCredentials()` should return the correct credentials based on whether the managed identity is enabled', () => {
const resultForManagedIdentityEnabled = getDefaultCredentials(true, AzureCloud.Public); jest.mocked(config).azure.managedIdentityEnabled = true;
const resultForManagedIdentityDisabled = getDefaultCredentials(false, AzureCloud.Public); const resultForManagedIdentityEnabled = getDefaultCredentials();
jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForManagedIdentityDisabled = getDefaultCredentials();
expect(resultForManagedIdentityEnabled).toEqual({ authType: 'msi' }); expect(resultForManagedIdentityEnabled).toEqual({ authType: 'msi' });
expect(resultForManagedIdentityDisabled).toEqual({ authType: 'clientsecret', azureCloud: 'AzureCloud' }); 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()', () => { describe('getCredentials()', () => {
it('should return the correct managed identity credentials', () => { 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( const resultForManagedIdentityEnabled = getCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithMsiCredentials, dataSourceSettingsWithMsiCredentials
configWithManagedIdentityEnabled
); );
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. // Default to basic client secret credentials.
jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForManagedIdentityEnabledInJSONButDisabledInConfig = getCredentials( const resultForManagedIdentityEnabledInJSONButDisabledInConfig = getCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithMsiCredentials, dataSourceSettingsWithMsiCredentials
configWithManagedIdentityDisabled
); );
expect(resultForManagedIdentityEnabledInJSONButDisabledInConfig).toEqual({ expect(resultForManagedIdentityEnabledInJSONButDisabledInConfig).toEqual({
authType: AzureAuthType.CLIENT_SECRET, authType: 'clientsecret',
azureCloud: 'AzureCloud', azureCloud: 'AzureCloud',
}); });
}); });
it('should return the correct client secret credentials', () => { it('should return the correct client secret credentials', () => {
const basicExpectedResult = { const basicExpectedResult = {
authType: AzureAuthType.CLIENT_SECRET, authType: 'clientsecret',
azureCloud: 'AzureCloud', azureCloud: 'AzureCloud',
tenantId: 'XXXX-tenant-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX',
clientId: 'XXXX-client-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. // i.e. the client secret is stored on the server.
jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForClientSecretCredentialsOnServer = getCredentials( const resultForClientSecretCredentialsOnServer = getCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithClientSecretOnServer, dataSourceSettingsWithClientSecretOnServer
configWithManagedIdentityDisabled
); );
// Here we test the properties separately because the client secret is a symbol, // 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. // and since JS symobls are unique, we test via the `typeof` operator.
expect(resultForClientSecretCredentialsOnServer.authType).toEqual(AzureAuthType.CLIENT_SECRET); expect(resultForClientSecretCredentialsOnServer.authType).toEqual('clientsecret');
expect(resultForClientSecretCredentialsOnServer.azureCloud).toEqual('AzureCloud'); expect(
expect(resultForClientSecretCredentialsOnServer.tenantId).toEqual('XXXX-tenant-id-XXXX'); instanceOfAzureCredential<AzureClientSecretCredentials>(
expect(resultForClientSecretCredentialsOnServer.clientId).toEqual('XXXX-client-id-XXXX'); 'clientsecret',
expect(typeof resultForClientSecretCredentialsOnServer.clientSecret).toEqual('symbol'); 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. // i.e. the client secret is stored in the secureJson.
jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForClientSecretCredentialsInSecureJSON = getCredentials( const resultForClientSecretCredentialsInSecureJSON = getCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithClientSecretInSecureJSONData, dataSourceSettingsWithClientSecretInSecureJSONData
configWithManagedIdentityDisabled
); );
expect(resultForClientSecretCredentialsInSecureJSON).toEqual({ expect(resultForClientSecretCredentialsInSecureJSON).toEqual({
...basicExpectedResult, ...basicExpectedResult,
@ -97,66 +120,68 @@ describe('AzureAuth', () => {
describe('updateCredentials()', () => { describe('updateCredentials()', () => {
it('should update the credentials for managed service identity correctly', () => { it('should update the credentials for managed service identity correctly', () => {
// If `dataSourceSettings.authType === AzureAuthType.MSI` && `config.azure.managedIdentityEnabled === true`. // If `dataSourceSettings.authType === 'msi'` && `config.azure.managedIdentityEnabled === true`.
const resultForMsiCredentials = updateCredentials( jest.mocked(config).azure.managedIdentityEnabled = true;
const resultForMsiCredentials = updateDatasourceCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithMsiCredentials, dataSourceSettingsWithMsiCredentials,
configWithManagedIdentityEnabled,
{ {
authType: AzureAuthType.MSI, authType: 'msi',
} }
); );
expect(resultForMsiCredentials).toEqual({ jsonData: { azureCredentials: { 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(() => expect(() =>
updateCredentials( updateDatasourceCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithMsiCredentials, dataSourceSettingsWithMsiCredentials,
configWithManagedIdentityDisabled,
{ {
authType: AzureAuthType.MSI, authType: 'msi',
} }
) )
).toThrow('Managed Identity authentication is not enabled in Grafana config.'); ).toThrow('Managed Identity authentication is not enabled in Grafana config.');
}); });
it('should update the credentials for client secret correctly', () => { it('should update the credentials for client secret correctly', () => {
const basicClientSecretCredentials: AzureCredentialsType = { const basicClientSecretCredentials: AzureCredentials = {
authType: AzureAuthType.CLIENT_SECRET, authType: 'clientsecret',
azureCloud: 'AzureCloud', azureCloud: AzureCloud.Public,
tenantId: 'XXXX-tenant-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX',
clientId: 'XXXX-client-id-XXXX', clientId: 'XXXX-client-id-XXXX',
}; };
// If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == true`. // If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == true`.
const resultForClientSecretCredentials1 = updateCredentials( jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForClientSecretCredentials1 = updateDatasourceCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithClientSecretOnServer, dataSourceSettingsWithClientSecretOnServer,
configWithManagedIdentityDisabled,
basicClientSecretCredentials basicClientSecretCredentials
); );
expect(resultForClientSecretCredentials1).toEqual({
jsonData: { expect(resultForClientSecretCredentials1.jsonData.azureCredentials).toEqual(basicClientSecretCredentials);
azureCredentials: { ...basicClientSecretCredentials }, expect(resultForClientSecretCredentials1.secureJsonData).toEqual({ azureClientSecret: undefined });
}, expect(resultForClientSecretCredentials1.secureJsonFields).toEqual({
secureJsonData: { azureClientSecret: undefined }, azureClientSecret: false,
secureJsonFields: { azureClientSecret: false }, clientSecret: false,
}); });
// If `dataSourceSettings.authType === AzureAuthType.CLIENT_SECRET` && `secureJsonFields.azureClientSecret == false`. // If `dataSourceSettings.authType === 'clientsecret'` && `secureJsonFields.azureClientSecret == false`.
const resultForClientSecretCredentials2 = updateCredentials( jest.mocked(config).azure.managedIdentityEnabled = false;
const resultForClientSecretCredentials2 = updateDatasourceCredentials(
// @ts-ignore // @ts-ignore
dataSourceSettingsWithClientSecretInSecureJSONData, dataSourceSettingsWithClientSecretInSecureJSONData,
configWithManagedIdentityDisabled,
{ ...basicClientSecretCredentials, clientSecret: 'XXXX-super-secret-secret-XXXX' } { ...basicClientSecretCredentials, clientSecret: 'XXXX-super-secret-secret-XXXX' }
); );
expect(resultForClientSecretCredentials2).toEqual({
jsonData: { expect(resultForClientSecretCredentials2.jsonData.azureCredentials).toEqual(basicClientSecretCredentials);
azureCredentials: { ...basicClientSecretCredentials }, expect(resultForClientSecretCredentials2.secureJsonData).toEqual({
}, azureClientSecret: 'XXXX-super-secret-secret-XXXX',
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' }, });
secureJsonFields: { azureClientSecret: false }, 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 { GrafanaBootConfig } from '@grafana/runtime';
import { AzureAuthSecureJSONDataType, AzureAuthJSONDataType, AzureAuthType } from '../types';
export const configWithManagedIdentityEnabled: Partial<GrafanaBootConfig> = { export const configWithManagedIdentityEnabled: Partial<GrafanaBootConfig> = {
azure: { azure: {
managedIdentityEnabled: true, managedIdentityEnabled: true,
@ -24,31 +22,22 @@ export const configWithManagedIdentityDisabled: Partial<GrafanaBootConfig> = {
}, },
}; };
export const dataSourceSettingsWithMsiCredentials: Partial< export const dataSourceSettingsWithMsiCredentials: Partial<AzureDataSourceSettings> = {
DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType> jsonData: { azureCredentials: { authType: 'msi' } },
> = {
jsonData: { azureCredentials: { authType: AzureAuthType.MSI } },
}; };
const basicJSONData = { // Will return symbol as the secret is concealed
export const dataSourceSettingsWithClientSecretOnServer: Partial<AzureDataSourceSettings> = {
jsonData: { jsonData: {
azureCredentials: { azureCredentials: { authType: 'clientsecret', clientId: 'XXXX-client-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX' },
authType: AzureAuthType.CLIENT_SECRET,
tenantId: 'XXXX-tenant-id-XXXX',
clientId: 'XXXX-client-id-XXXX',
},
}, },
secureJsonFields: { azureClientSecret: true },
}; };
// 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 // Will return the secret as a string from the secureJsonData
export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial< export const dataSourceSettingsWithClientSecretInSecureJSONData: Partial<AzureDataSourceSettings> = {
DataSourceSettings<AzureAuthJSONDataType, AzureAuthSecureJSONDataType> jsonData: {
> = { azureCredentials: { authType: 'clientsecret', clientId: 'XXXX-client-id-XXXX', tenantId: 'XXXX-tenant-id-XXXX' },
...basicJSONData, },
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX', password: undefined }, secureJsonFields: { azureClientSecret: false },
secureJsonData: { azureClientSecret: 'XXXX-super-secret-secret-XXXX' },
}; };

@ -1,24 +1,25 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useEffectOnce } from 'react-use'; import { useEffectOnce } from 'react-use';
import { AzureCredentials, AzureCloud, updateDatasourceCredentials } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types'; import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
import { AzureCredentialsType } from '../types'; import { getCredentials } from './AzureCredentialsConfig';
import { KnownAzureClouds } from './AzureCredentials';
import { getCredentials, updateCredentials } from './AzureCredentialsConfig';
import { AzureCredentialsForm } from './AzureCredentialsForm'; import { AzureCredentialsForm } from './AzureCredentialsForm';
export const KnownAzureClouds: Array<SelectableValue<AzureCloud>> = [{ value: AzureCloud.Public, label: 'Azure' }];
export const AzureAuthSettings = (props: HttpSettingsBaseProps) => { export const AzureAuthSettings = (props: HttpSettingsBaseProps) => {
const { dataSourceConfig: dsSettings, onChange } = props; const { dataSourceConfig: dsSettings, onChange } = props;
const managedIdentityEnabled = config.azure.managedIdentityEnabled; const managedIdentityEnabled = config.azure.managedIdentityEnabled;
const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled; const azureEntraPasswordCredentialsEnabled = config.azure.azureEntraPasswordCredentialsEnabled;
const credentials = useMemo(() => getCredentials(dsSettings, config), [dsSettings]); const credentials = useMemo(() => getCredentials(dsSettings), [dsSettings]);
const onCredentialsChange = (credentials: AzureCredentialsType): void => { const onCredentialsChange = (credentials: AzureCredentials): void => {
onChange(updateCredentials(dsSettings, config, credentials)); onChange(updateDatasourceCredentials(dsSettings, credentials));
}; };
// The auth type needs to be set on the first load of the data source // 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 { import {
AzureCloud, AzureCredentials,
AzureCredentialsType, AzureDataSourceSettings,
ConcealedSecretType, getDatasourceCredentials,
AzureAuthSecureJSONDataType, getDefaultAzureCloud,
AzureAuthJSONDataType, } from '@grafana/azure-sdk';
AzureAuthType, import { config } from '@grafana/runtime';
} from '../types';
export const getDefaultCredentials = (): AzureCredentials => {
export const getDefaultCredentials = (managedIdentityEnabled: boolean, cloud: string): AzureCredentialsType => { if (config.azure.managedIdentityEnabled) {
if (managedIdentityEnabled) { return { authType: 'msi' };
return { authType: AzureAuthType.MSI };
} else { } else {
return { authType: AzureAuthType.CLIENT_SECRET, azureCloud: cloud }; return { authType: 'clientsecret', azureCloud: getDefaultAzureCloud() };
} }
}; };
export const getSecret = ( export const getCredentials = (dsSettings: AzureDataSourceSettings): AzureCredentials => {
storedServerSide: boolean, const credentials = getDatasourceCredentials(dsSettings);
secret: string | symbol | undefined if (credentials) {
): undefined | string | ConcealedSecretType => { return credentials;
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: 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 // If no credentials saved, then return empty credentials
// of type based on whether the managed identity enabled // of type based on whether the managed identity enabled
if (!credentials) { return getDefaultCredentials();
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',
},
};
}
}; };

@ -1,16 +1,15 @@
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import { AzureCredentials, AzureAuthType } from '@grafana/azure-sdk';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Button, Field, Select, Input } from '@grafana/ui/src/components'; import { Button, Field, Select, Input } from '@grafana/ui/src/components';
import { AzureCredentialsType, AzureAuthType } from '../types';
export interface Props { export interface Props {
managedIdentityEnabled: boolean; managedIdentityEnabled: boolean;
azureEntraPasswordCredentialsEnabled: boolean; azureEntraPasswordCredentialsEnabled: boolean;
credentials: AzureCredentialsType; credentials: AzureCredentials;
azureCloudOptions?: SelectableValue[]; azureCloudOptions?: SelectableValue[];
onCredentialsChange: (updatedCredentials: AzureCredentialsType) => void; onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
disabled?: boolean; disabled?: boolean;
} }
@ -26,9 +25,89 @@ export const AzureCredentialsForm = (props: Props) => {
const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => { const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
if (onCredentialsChange) { if (onCredentialsChange) {
const updated: AzureCredentialsType = { const updated: AzureCredentials = {
...credentials,
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, ...credentials,
authType: selected.value || AzureAuthType.MSI, password: '',
}; };
onCredentialsChange(updated); onCredentialsChange(updated);
} }
@ -36,33 +115,23 @@ export const AzureCredentialsForm = (props: Props) => {
const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [ const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
{ {
value: AzureAuthType.CLIENT_SECRET, value: 'clientsecret',
label: 'App Registration', label: 'App Registration',
}, },
]; ];
if (managedIdentityEnabled) { if (managedIdentityEnabled) {
authTypeOptions.push({ authTypeOptions.push({
value: AzureAuthType.MSI, value: 'msi',
label: 'Managed Identity', label: 'Managed Identity',
}); });
} }
if (azureEntraPasswordCredentialsEnabled) { if (azureEntraPasswordCredentialsEnabled) {
authTypeOptions.push({ authTypeOptions.push({
value: AzureAuthType.AD_PASSWORD, value: 'ad-password',
label: 'Azure Entra 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 ( return (
<div> <div>
<Field <Field
@ -78,17 +147,14 @@ export const AzureCredentialsForm = (props: Props) => {
disabled={disabled} disabled={disabled}
/> />
</Field> </Field>
{credentials.authType === AzureAuthType.CLIENT_SECRET && ( {credentials.authType === 'clientsecret' && (
<> <>
{azureCloudOptions && ( {azureCloudOptions && (
<Field label="Azure Cloud" htmlFor="azure-cloud-type" disabled={disabled}> <Field label="Azure Cloud" htmlFor="azure-cloud-type" disabled={disabled}>
<Select <Select
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)} value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions} options={azureCloudOptions}
onChange={(selected: SelectableValue<AzureAuthType>) => { onChange={onAzureCloudChange}
const value = selected.value || '';
onInputChange({ property: 'azureCloud', value });
}}
isDisabled={disabled} isDisabled={disabled}
inputId="azure-cloud-type" inputId="azure-cloud-type"
aria-label="Azure Cloud" aria-label="Azure Cloud"
@ -107,10 +173,7 @@ export const AzureCredentialsForm = (props: Props) => {
width={45} width={45}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''} value={credentials.tenantId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={onTenantIdChange}
const value = event.target.value;
onInputChange({ property: 'tenantId', value });
}}
disabled={disabled} disabled={disabled}
aria-label="Tenant ID" aria-label="Tenant ID"
/> />
@ -126,10 +189,7 @@ export const AzureCredentialsForm = (props: Props) => {
width={45} width={45}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''} value={credentials.clientId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={onClientIdChange}
const value = event.target.value;
onInputChange({ property: 'clientId', value });
}}
disabled={disabled} disabled={disabled}
aria-label="Client ID" aria-label="Client ID"
/> />
@ -145,14 +205,7 @@ export const AzureCredentialsForm = (props: Props) => {
data-testid={'client-secret'} data-testid={'client-secret'}
width={45} width={45}
/> />
<Button <Button variant="secondary" type="button" onClick={onClientSecretReset} disabled={disabled}>
variant="secondary"
type="button"
onClick={() => {
onInputChange({ property: 'clientSecret', value: '' });
}}
disabled={disabled}
>
Reset Reset
</Button> </Button>
</div> </div>
@ -170,10 +223,7 @@ export const AzureCredentialsForm = (props: Props) => {
aria-label="Client Secret" aria-label="Client Secret"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientSecret || ''} value={credentials.clientSecret || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={onClientSecretChange}
const value = event.target.value;
onInputChange({ property: 'clientSecret', value });
}}
id="client-secret" id="client-secret"
disabled={disabled} 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'}> <Field label="User Id" required htmlFor="user-id" invalid={!credentials.userId} error={'User ID is required'}>
<Input <Input
width={45} width={45}
value={credentials.userId || ''} value={credentials.userId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={onUserIdChange}
const value = event.target.value;
onInputChange({ property: 'userId', value });
}}
disabled={disabled} disabled={disabled}
aria-label="User ID" aria-label="User ID"
/> />
@ -205,10 +252,7 @@ export const AzureCredentialsForm = (props: Props) => {
<Input <Input
width={45} width={45}
value={credentials.clientId || ''} value={credentials.clientId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={onClientIdChange}
const value = event.target.value;
onInputChange({ property: 'clientId', value });
}}
disabled={disabled} disabled={disabled}
aria-label="Application Client ID" aria-label="Application Client ID"
/> />
@ -224,14 +268,7 @@ export const AzureCredentialsForm = (props: Props) => {
data-testid={'password'} data-testid={'password'}
width={45} width={45}
/> />
<Button <Button variant="secondary" type="button" onClick={onPasswordReset} disabled={disabled}>
variant="secondary"
type="button"
onClick={() => {
onInputChange({ property: 'password', value: '' });
}}
disabled={disabled}
>
Reset Reset
</Button> </Button>
</div> </div>
@ -248,10 +285,7 @@ export const AzureCredentialsForm = (props: Props) => {
width={45} width={45}
aria-label="Password" aria-label="Password"
value={credentials.password || ''} value={credentials.password || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => { onChange={onPasswordChange}
const value = event.target.value;
onInputChange({ property: 'password', value });
}}
id="password" id="password"
disabled={disabled} disabled={disabled}
/> />

@ -1,4 +1,4 @@
import { DataSourceJsonData } from '@grafana/data'; import { AzureCredentials } from '@grafana/azure-sdk';
import { SQLOptions } from '@grafana/sql'; import { SQLOptions } from '@grafana/sql';
import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types'; import { HttpSettingsBaseProps } from '@grafana/ui/src/components/DataSourceSettings/types';
@ -17,37 +17,13 @@ export enum MSSQLEncryptOptions {
false = 'false', false = 'false',
true = 'true', 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 { export interface MssqlOptions extends SQLOptions {
authenticationType?: MSSQLAuthenticationType; authenticationType?: MSSQLAuthenticationType;
encrypt?: MSSQLEncryptOptions; encrypt?: MSSQLEncryptOptions;
sslRootCertFile?: string; sslRootCertFile?: string;
serverName?: string; serverName?: string;
connectionTimeout?: number; connectionTimeout?: number;
azureCredentials?: AzureCredentialsType; azureCredentials?: AzureCredentials;
keytabFilePath?: string; keytabFilePath?: string;
credentialCache?: string; credentialCache?: string;
credentialCacheLookupFile?: string; credentialCacheLookupFile?: string;
@ -60,15 +36,6 @@ export interface MssqlSecureOptions {
password?: string; password?: string;
} }
export type AzureAuthJSONDataType = DataSourceJsonData & {
azureCredentials: AzureCredentialsType;
};
export type AzureAuthSecureJSONDataType = {
azureClientSecret: undefined | string | ConcealedSecretType;
password: undefined | string | ConcealedSecretType;
};
export type AzureAuthConfigType = { export type AzureAuthConfigType = {
azureAuthIsSupported: boolean; azureAuthIsSupported: boolean;
azureAuthSettingsUI: (props: HttpSettingsBaseProps) => JSX.Element; azureAuthSettingsUI: (props: HttpSettingsBaseProps) => JSX.Element;

Loading…
Cancel
Save