MSSQL: ConfigEditor updates (#75275)

* Add secure json data type

* Update Azure credentials form with Field components

- Update labels
- Update widths
- Remove excess code

* Update config editor

* Fix lint
This commit is contained in:
Andreas Christou 2023-09-25 14:14:41 +01:00 committed by GitHub
parent 6811b0ae63
commit 61e3f3a059
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 283 additions and 253 deletions

View File

@ -1,9 +1,7 @@
import React, { ChangeEvent } from 'react';
import { SelectableValue } from '@grafana/data';
import { InlineFormLabel, Button } from '@grafana/ui/src/components';
import { Input } from '@grafana/ui/src/components/Forms/Legacy/Input/Input';
import { Select } from '@grafana/ui/src/components/Forms/Legacy/Select/Select';
import { Button, Field, Select, Input } from '@grafana/ui/src/components';
import { AzureCredentialsType, AzureAuthType } from '../types';
@ -52,117 +50,121 @@ export const AzureCredentialsForm = (props: Props) => {
return (
<div>
{managedIdentityEnabled && (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel tooltip="Choose the type of authentication to Azure services">
Authentication
</InlineFormLabel>
<Select
value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
options={authTypeOptions}
onChange={onAuthTypeChange}
isDisabled={disabled}
/>
</div>
</div>
<Field
label="Authentication"
description="Choose the type of authentication to Azure services"
htmlFor="authentication-type"
>
<Select
width={20}
value={authTypeOptions.find((opt) => opt.value === credentials.authType)}
options={authTypeOptions}
onChange={onAuthTypeChange}
disabled={disabled}
/>
</Field>
)}
{credentials.authType === 'clientsecret' && (
<>
{azureCloudOptions && (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12" tooltip="Choose an Azure Cloud">
Azure Cloud
</InlineFormLabel>
<Select
value={azureCloudOptions.find((opt) => opt.value === credentials.azureCloud)}
options={azureCloudOptions}
onChange={(selected: SelectableValue<AzureAuthType>) => {
const value = selected.value || '';
onInputChange({ property: 'azureCloud', value });
}}
isDisabled={disabled}
/>
</div>
</div>
<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 });
}}
isDisabled={disabled}
inputId="azure-cloud-type"
aria-label="Azure Cloud"
width={20}
/>
</Field>
)}
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Directory (tenant) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'tenantId', value });
}}
disabled={disabled}
/>
</div>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Application (client) ID</InlineFormLabel>
<div className="width-15">
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'clientId', value });
}}
disabled={disabled}
/>
</div>
</div>
</div>
{typeof credentials.clientSecret === 'symbol' ? (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel htmlFor="azure-client-secret" className="width-12">
Client Secret
</InlineFormLabel>
<Input id="azure-client-secret" className="width-25" placeholder="configured" disabled />
</div>
{!disabled && (
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button
variant="secondary"
type="button"
onClick={() => {
onInputChange({ property: 'clientSecret', value: '' });
}}
>
reset
</Button>
</div>
</div>
)}
</div>
) : (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-12">Client Secret</InlineFormLabel>
<div className="width-15">
<Field
label="Directory (tenant) ID"
required
htmlFor="tenant-id"
invalid={!credentials.tenantId}
error={'Tenant ID is required'}
>
<Input
width={45}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.tenantId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'tenantId', value });
}}
disabled={disabled}
aria-label="Tenant ID"
/>
</Field>
<Field
label="Application (client) ID"
required
htmlFor="client-id"
invalid={!credentials.clientId}
error={'Client ID is required'}
>
<Input
width={45}
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientId || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'clientId', value });
}}
disabled={disabled}
aria-label="Client ID"
/>
</Field>
{!disabled &&
(typeof credentials.clientSecret === 'symbol' ? (
<Field label="Client Secret" htmlFor="client-secret" required>
<div className="width-30" style={{ display: 'flex', gap: '4px' }}>
<Input
className="width-30"
placeholder="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
value={credentials.clientSecret || ''}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
onInputChange({ property: 'clientSecret', value });
aria-label="Client Secret"
placeholder="configured"
disabled={true}
data-testid={'client-secret'}
width={45}
/>
<Button
variant="secondary"
type="button"
onClick={() => {
onInputChange({ property: 'clientSecret', value: '' });
}}
disabled={disabled}
/>
>
Reset
</Button>
</div>
</div>
</div>
)}
</Field>
) : (
<Field
label="Client Secret"
required
htmlFor="client-secret"
invalid={!credentials.clientSecret}
error={'Client secret is required'}
>
<Input
width={45}
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 });
}}
id="client-secret"
disabled={disabled}
/>
</Field>
))}
</>
)}
</div>

View File

@ -10,18 +10,19 @@ import {
updateDatasourcePluginJsonDataOption,
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { ConfigSection, ConfigSubSection, DataSourceDescription } from '@grafana/experimental';
import {
Alert,
FieldSet,
InlineField,
InlineFieldRow,
InlineSwitch,
Input,
Link,
SecretInput,
Select,
useStyles2,
SecureSocksProxySettings,
Divider,
Field,
Switch,
} from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { config } from 'app/core/config';
@ -29,14 +30,18 @@ import { ConnectionLimits } from 'app/features/plugins/sql/components/configurat
import { useMigrateDatabaseFields } from 'app/features/plugins/sql/components/configuration/useMigrateDatabaseFields';
import { AzureAuthSettings } from '../azureauth/AzureAuthSettings';
import { MSSQLAuthenticationType, MSSQLEncryptOptions, MssqlOptions, AzureAuthConfigType } from '../types';
import {
MSSQLAuthenticationType,
MSSQLEncryptOptions,
MssqlOptions,
AzureAuthConfigType,
MssqlSecureOptions,
} from '../types';
const SHORT_WIDTH = 15;
const LONG_WIDTH = 46;
const LABEL_WIDTH_SSL = 25;
const LABEL_WIDTH_DETAILS = 20;
const LONG_WIDTH = 40;
export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<MssqlOptions>) => {
export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<MssqlOptions, MssqlSecureOptions>) => {
useMigrateDatabaseFields(props);
const { options: dsSettings, onOptionsChange } = props;
@ -107,8 +112,25 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
return (
<>
<FieldSet label="MS SQL Connection" width={400}>
<InlineField labelWidth={SHORT_WIDTH} label="Host">
<DataSourceDescription
dataSourceName="Microsoft SQL Server"
docsLink="https://grafana.com/docs/grafana/latest/datasources/mssql/"
hasRequiredFields
/>
<Alert title="User Permission" severity="info">
The database user should only be granted SELECT permissions on the specified database and tables you want to
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect
against this we <em>highly</em> recommend you create a specific MS SQL user with restricted permissions. Check
out the{' '}
<Link rel="noreferrer" target="_blank" href="http://docs.grafana.org/features/datasources/mssql/">
Microsoft SQL Server Data Source Docs
</Link>{' '}
for more information.
</Alert>
<Divider />
<ConfigSection title="Connection">
<Field label="Host" required invalid={!dsSettings.url} error={'Host is required'}>
<Input
width={LONG_WIDTH}
name="host"
@ -116,82 +138,23 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
value={dsSettings.url || ''}
placeholder="localhost:1433"
onChange={onDSOptionChanged('url')}
></Input>
</InlineField>
<InlineField labelWidth={SHORT_WIDTH} label="Database">
/>
</Field>
<Field label="Database" required invalid={!jsonData.database} error={'Database is required'}>
<Input
width={LONG_WIDTH}
name="database"
value={jsonData.database || ''}
placeholder="database name"
onChange={onUpdateDatasourceJsonDataOption(props, 'database')}
></Input>
</InlineField>
<InlineField
label="Authentication"
labelWidth={SHORT_WIDTH}
htmlFor="authenticationType"
tooltip={
<ul className={styles.ulPadding}>
<li>
<i>SQL Server Authentication</i> This is the default mechanism to connect to MS SQL Server. Enter the
SQL Server Authentication login or the Windows Authentication login in the DOMAIN\User format.
</li>
<li>
<i>Windows Authentication</i> Windows Integrated Security - single sign on for users who are already
logged onto Windows and have enabled this option for MS SQL Server.
</li>
{azureAuthIsSupported && (
<li>
<i>Azure Authentication</i> Securely authenticate and access Azure resources and applications using
Azure AD credentials - Managed Service Identity and Client Secret Credentials are supported.
</li>
)}
</ul>
}
>
<Select
// Default to basic authentication of none is set
value={jsonData.authenticationType || MSSQLAuthenticationType.sqlAuth}
inputId="authenticationType"
options={buildAuthenticationOptions()}
onChange={onAuthenticationMethodChanged}
></Select>
</InlineField>
{/* Basic SQL auth. Render if authType === MSSQLAuthenticationType.sqlAuth OR
if no authType exists, which will be the case when creating a new data source */}
{(jsonData.authenticationType === MSSQLAuthenticationType.sqlAuth || !jsonData.authenticationType) && (
<InlineFieldRow>
<InlineField labelWidth={SHORT_WIDTH} label="User">
<Input
width={SHORT_WIDTH}
value={dsSettings.user || ''}
placeholder="user"
onChange={onDSOptionChanged('user')}
></Input>
</InlineField>
<InlineField label="Password" labelWidth={SHORT_WIDTH}>
<SecretInput
width={SHORT_WIDTH}
placeholder="Password"
isConfigured={dsSettings.secureJsonFields && dsSettings.secureJsonFields.password}
onReset={onResetPassword}
onBlur={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
></SecretInput>
</InlineField>
</InlineFieldRow>
)}
</FieldSet>
/>
</Field>
</ConfigSection>
{config.secureSocksDSProxyEnabled && (
<SecureSocksProxySettings options={dsSettings} onOptionsChange={onOptionsChange} />
)}
<FieldSet label="TLS/SSL Auth">
<InlineField
labelWidth={LABEL_WIDTH_SSL}
<ConfigSection title="TLS/SSL Auth">
<Field
htmlFor="encrypt"
tooltip={
description={
<>
Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server.
<ul className={styles.ulPadding}>
@ -216,23 +179,19 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
value={jsonData.encrypt || MSSQLEncryptOptions.false}
inputId="encrypt"
onChange={onEncryptChanged}
></Select>
</InlineField>
width={LONG_WIDTH}
/>
</Field>
{jsonData.encrypt === MSSQLEncryptOptions.true ? (
<>
<InlineField labelWidth={LABEL_WIDTH_SSL} htmlFor="skipTlsVerify" label="Skip TLS Verify">
<InlineSwitch
id="skipTlsVerify"
onChange={onSkipTLSVerifyChanged}
value={jsonData.tlsSkipVerify || false}
></InlineSwitch>
</InlineField>
<Field htmlFor="skipTlsVerify" label="Skip TLS Verify">
<Switch id="skipTlsVerify" onChange={onSkipTLSVerifyChanged} value={jsonData.tlsSkipVerify || false} />
</Field>
{jsonData.tlsSkipVerify ? null : (
<>
<InlineField
labelWidth={LABEL_WIDTH_SSL}
tooltip={
<Field
description={
<span>
Path to file containing the public key certificate of the CA that signed the SQL Server
certificate. Needed when the server certificate is self signed.
@ -244,76 +203,141 @@ export const ConfigurationEditor = (props: DataSourcePluginOptionsEditorProps<Ms
value={jsonData.sslRootCertFile || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'sslRootCertFile')}
placeholder="TLS/SSL root certificate file path"
></Input>
</InlineField>
<InlineField labelWidth={LABEL_WIDTH_SSL} label="Hostname in server certificate">
width={LONG_WIDTH}
/>
</Field>
<Field label="Hostname in server certificate">
<Input
placeholder="Common Name (CN) in server certificate"
value={jsonData.serverName || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'serverName')}
></Input>
</InlineField>
width={LONG_WIDTH}
/>
</Field>
</>
)}
</>
) : null}
</FieldSet>
</ConfigSection>
{azureAuthIsSupported && jsonData.authenticationType === MSSQLAuthenticationType.azureAuth && (
<FieldSet label="Azure Authentication Settings">
<azureAuthSettings.azureAuthSettingsUI dataSourceConfig={dsSettings} onChange={onOptionsChange} />
</FieldSet>
)}
<ConnectionLimits labelWidth={SHORT_WIDTH} options={dsSettings} onOptionsChange={onOptionsChange} />
<FieldSet label="MS SQL details">
<InlineField
tooltip={
<span>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example
<code>1m</code> if your data is written every minute.
</span>
<ConfigSection title="Authentication">
<Field
label="Authentication Type"
htmlFor="authenticationType"
description={
<ul className={styles.ulPadding}>
<li>
<i>SQL Server Authentication</i> This is the default mechanism to connect to MS SQL Server. Enter the
SQL Server Authentication login or the Windows Authentication login in the DOMAIN\User format.
</li>
<li>
<i>Windows Authentication</i> Windows Integrated Security - single sign on for users who are already
logged onto Windows and have enabled this option for MS SQL Server.
</li>
{azureAuthIsSupported && (
<li>
<i>Azure Authentication</i> Securely authenticate and access Azure resources and applications using
Azure AD credentials - Managed Service Identity and Client Secret Credentials are supported.
</li>
)}
</ul>
}
label="Min time interval"
labelWidth={LABEL_WIDTH_DETAILS}
>
<Input
placeholder="1m"
value={jsonData.timeInterval || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
></Input>
</InlineField>
<InlineField
tooltip={
<span>
The number of seconds to wait before canceling the request when connecting to the database. The default is{' '}
<code>0</code>, meaning no timeout.
</span>
}
label="Connection timeout"
labelWidth={LABEL_WIDTH_DETAILS}
>
<NumberInput
placeholder="60"
min={0}
value={jsonData.connectionTimeout}
onChange={onConnectionTimeoutChanged}
></NumberInput>
</InlineField>
</FieldSet>
<Select
// Default to basic authentication of none is set
value={jsonData.authenticationType || MSSQLAuthenticationType.sqlAuth}
inputId="authenticationType"
options={buildAuthenticationOptions()}
onChange={onAuthenticationMethodChanged}
width={LONG_WIDTH}
/>
</Field>
<Alert title="User Permission" severity="info">
The database user should only be granted SELECT permissions on the specified database and tables you want to
query. Grafana does not validate that queries are safe so queries can contain any SQL statement. For example,
statements like <code>USE otherdb;</code> and <code>DROP TABLE user;</code> would be executed. To protect
against this we <em>highly</em> recommend you create a specific MS SQL user with restricted permissions. Check
out the{' '}
<Link rel="noreferrer" target="_blank" href="http://docs.grafana.org/features/datasources/mssql/">
Microsoft SQL Server Data Source Docs
</Link>{' '}
for more information.
</Alert>
{/* Basic SQL auth. Render if authType === MSSQLAuthenticationType.sqlAuth OR
if no authType exists, which will be the case when creating a new data source */}
{(jsonData.authenticationType === MSSQLAuthenticationType.sqlAuth || !jsonData.authenticationType) && (
<>
<Field label="Username" required invalid={!dsSettings.user} error={'Username is required'}>
<Input
value={dsSettings.user || ''}
placeholder="user"
onChange={onDSOptionChanged('user')}
width={LONG_WIDTH}
/>
</Field>
<Field
label="Password"
required
invalid={!dsSettings.secureJsonFields.password && !dsSettings.secureJsonData?.password}
error={'Password is required'}
>
<SecretInput
width={LONG_WIDTH}
placeholder="Password"
isConfigured={dsSettings.secureJsonFields && dsSettings.secureJsonFields.password}
onReset={onResetPassword}
onChange={onUpdateDatasourceSecureJsonDataOption(props, 'password')}
required
/>
</Field>
</>
)}
{azureAuthIsSupported && jsonData.authenticationType === MSSQLAuthenticationType.azureAuth && (
<FieldSet label="Azure Authentication Settings">
<azureAuthSettings.azureAuthSettingsUI dataSourceConfig={dsSettings} onChange={onOptionsChange} />
</FieldSet>
)}
</ConfigSection>
<Divider />
<ConfigSection
title="Additional settings"
description="Additional settings are optional settings that can be configured for more control over your data source. This includes connection limits, connection timeout, group-by time interval, and Secure Socks Proxy."
isCollapsible={true}
isInitiallyOpen={true}
>
<ConnectionLimits labelWidth={SHORT_WIDTH} options={dsSettings} onOptionsChange={onOptionsChange} />
<ConfigSubSection title="Connection details">
<Field
description={
<span>
A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example
<code>1m</code> if your data is written every minute.
</span>
}
label="Min time interval"
>
<Input
width={LONG_WIDTH}
placeholder="1m"
value={jsonData.timeInterval || ''}
onChange={onUpdateDatasourceJsonDataOption(props, 'timeInterval')}
/>
</Field>
<Field
description={
<span>
The number of seconds to wait before canceling the request when connecting to the database. The default
is <code>0</code>, meaning no timeout.
</span>
}
label="Connection timeout"
>
<NumberInput
width={LONG_WIDTH}
placeholder="60"
min={0}
value={jsonData.connectionTimeout}
onChange={onConnectionTimeoutChanged}
/>
</Field>
</ConfigSubSection>
{config.secureSocksDSProxyEnabled && (
<SecureSocksProxySettings options={dsSettings} onOptionsChange={onOptionsChange} />
)}
</ConfigSection>
</>
);
};

View File

@ -43,6 +43,10 @@ export interface MssqlOptions extends SQLOptions {
azureCredentials?: AzureCredentialsType;
}
export interface MssqlSecureOptions {
password?: string;
}
export type AzureAuthJSONDataType = DataSourceJsonData & {
azureCredentials: AzureCredentialsType;
};