Auth: Add Sigv4 auth option to datasources (#27552)

* create transport chain

* add frontend

* remove log

* inline field updates

* allow ARN, Credentials + Keys auth in frontend

* configure credentials

* add tests and refactor

* update frontend json field names

* fix tests

* fix comment

* add app config flag

* refactor tests

* add return field for tests

* add flag for UI display

* update comment

* move logic

* fix config

* pass config through props

* update docs

* pr feedback and add docs coverage

* shorten settings filename

* fix imports

* revert docs changes

* remove log line

* wrap up next as round tripper

* only propagate required config

* remove unused import

* remove ARN option and replace with default chain

* make ARN role assume as supplemental

* update docs

* refactor flow

* sign body when necessary

* remove unnecessary wrapper

* remove newline

* Apply suggestions from code review

* PR fixes

Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Will Browne
2020-10-08 10:03:20 +02:00
committed by GitHub
parent ab33e46789
commit 7d63b2c473
17 changed files with 517 additions and 11 deletions

View File

@@ -13,7 +13,7 @@ It is used in a `ConfigEditor` for data source plugins. You can find more exampl
### Example usage
```jsx
export const ConfigEditor = (props: Props) => {
const { options, onOptionsChange } = props;
const { options, onOptionsChange, config } = props;
return (
<>
<DataSourceHttpSettings
@@ -21,6 +21,7 @@ export const ConfigEditor = (props: Props) => {
dataSourceConfig={options}
showAccessOptions={true}
onChange={onOptionsChange}
sigV4AuthEnabled={false}
/>
{/* Additional configuration settings for your data source plugin.*/}

View File

@@ -12,6 +12,7 @@ import { Icon } from '../Icon/Icon';
import { FormField } from '../FormField/FormField';
import { InlineFormLabel } from '../FormLabel/FormLabel';
import { TagsInput } from '../TagsInput/TagsInput';
import { SigV4AuthSettings } from './SigV4AuthSettings';
import { useTheme } from '../../themes';
import { HttpSettingsProps } from './types';
@@ -55,7 +56,7 @@ const HttpAccessHelp = () => (
);
export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = props => {
const { defaultUrl, dataSourceConfig, onChange, showAccessOptions } = props;
const { defaultUrl, dataSourceConfig, onChange, showAccessOptions, sigV4AuthToggleEnabled } = props;
let urlTooltip;
const [isAccessHelpVisible, setIsAccessHelpVisible] = useState(false);
const theme = useTheme();
@@ -189,6 +190,21 @@ export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = props => {
/>
</div>
{sigV4AuthToggleEnabled && (
<div className="gf-form-inline">
<Switch
label="SigV4 auth"
labelClass="width-13"
checked={dataSourceConfig.jsonData.sigV4Auth || false}
onChange={event => {
onSettingsChange({
jsonData: { ...dataSourceConfig.jsonData, sigV4Auth: event!.currentTarget.checked },
});
}}
/>
</div>
)}
{dataSourceConfig.access === 'proxy' && (
<HttpProxySettings
dataSourceConfig={dataSourceConfig}
@@ -205,6 +221,8 @@ export const DataSourceHttpSettings: React.FC<HttpSettingsProps> = props => {
</>
)}
{dataSourceConfig.jsonData.sigV4Auth && <SigV4AuthSettings {...props} />}
{(dataSourceConfig.jsonData.tlsAuth || dataSourceConfig.jsonData.tlsAuthWithCACert) && (
<TLSAuthSettings dataSourceConfig={dataSourceConfig} onChange={onChange} />
)}

View File

@@ -0,0 +1,250 @@
import React from 'react';
import { HttpSettingsProps } from './types';
import { SelectableValue } from '@grafana/data';
import { Button, InlineFormLabel, Input } from '..';
import Select from '../Forms/Legacy/Select/Select';
export const SigV4AuthSettings: React.FC<HttpSettingsProps> = props => {
const { dataSourceConfig } = props;
const authProviderOptions = [
{ label: 'AWS SDK Default', value: 'default' },
{ label: 'Access & secret key', value: 'keys' },
{ label: 'Credentials file', value: 'credentials' },
] as SelectableValue[];
const regions = [
{ value: 'af-south-1', label: 'af-south-1' },
{ value: 'ap-east-1', label: 'ap-east-1' },
{ value: 'ap-northeast-1', label: 'ap-northeast-1' },
{ value: 'ap-northeast-2', label: 'ap-northeast-2' },
{ value: 'ap-northeast-3', label: 'ap-northeast-3' },
{ value: 'ap-south-1', label: 'ap-south-1' },
{ value: 'ap-southeast-1', label: 'ap-southeast-1' },
{ value: 'ap-southeast-2', label: 'ap-southeast-2' },
{ value: 'ca-central-1', label: 'ca-central-1' },
{ value: 'cn-north-1', label: 'cn-north-1' },
{ value: 'cn-northwest-1', label: 'cn-northwest-1' },
{ value: 'eu-central-1', label: 'eu-central-1' },
{ value: 'eu-north-1', label: 'eu-north-1' },
{ value: 'eu-west-1', label: 'eu-west-1' },
{ value: 'eu-west-2', label: 'eu-west-2' },
{ value: 'eu-west-3', label: 'eu-west-3' },
{ value: 'me-south-1', label: 'me-south-1' },
{ value: 'sa-east-1', label: 'sa-east-1' },
{ value: 'us-east-1', label: 'us-east-1' },
{ value: 'us-east-2', label: 'us-east-2' },
{ value: 'us-gov-east-1', label: 'us-gov-east-1' },
{ value: 'us-gov-west-1', label: 'us-gov-west-1' },
{ value: 'us-iso-east-1', label: 'us-iso-east-1' },
{ value: 'us-isob-east-1', label: 'us-isob-east-1' },
{ value: 'us-west-1', label: 'us-west-1' },
{ value: 'us-west-2', label: 'us-west-2' },
] as SelectableValue[];
const onSecureJsonDataReset = (fieldName: string) => {
const state = {
...dataSourceConfig,
secureJsonData: {
...dataSourceConfig.secureJsonData,
[fieldName]: '',
},
secureJsonFields: {
...dataSourceConfig.secureJsonFields,
[fieldName]: false,
},
};
props.onChange(state);
};
const onSecureJsonDataChange = (fieldName: string, fieldValue: string) => {
const state = {
...dataSourceConfig,
secureJsonData: {
...dataSourceConfig.secureJsonData,
[fieldName]: fieldValue,
},
};
props.onChange(state);
};
const onJsonDataChange = (fieldName: string, fieldValue: string) => {
const state = {
...dataSourceConfig,
jsonData: {
...dataSourceConfig.jsonData,
[fieldName]: fieldValue,
},
};
props.onChange(state);
};
return (
<>
<h3 className="page-heading">Sigv4 Details</h3>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
className="width-14"
tooltip="Which AWS credentials chain to use. AWS SDK Default is the recommended option for EKS, ECS, or if you've attached an IAM role to your EC2 instance."
>
Authentication Provider
</InlineFormLabel>
<Select
className="width-30"
value={authProviderOptions.find(
authProvider => authProvider.value === dataSourceConfig.jsonData.authType
)}
options={authProviderOptions}
defaultValue={dataSourceConfig.jsonData.authType || 'keys'}
onChange={option => {
if (dataSourceConfig.jsonData.authType === 'arn' && option.value !== 'arn') {
delete dataSourceConfig.jsonData.assumeRoleArn;
delete dataSourceConfig.jsonData.externalId;
}
onJsonDataChange('authType', option.value);
}}
/>
</div>
</div>
{dataSourceConfig.jsonData.authType === 'credentials' && (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
className="width-14"
tooltip="Credentials profile name, as specified in ~/.aws/credentials, leave blank for default."
>
Credentials Profile Name
</InlineFormLabel>
<div className="width-30">
<Input
className="width-30"
placeholder="default"
value={dataSourceConfig.jsonData.profile || ''}
onChange={e => onJsonDataChange('profile', e.currentTarget.value)}
/>
</div>
</div>
</div>
)}
{dataSourceConfig.jsonData.authType === 'keys' && (
<div>
{dataSourceConfig.secureJsonFields?.accessKey ? (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-14">Access Key ID</InlineFormLabel>
<Input className="width-25" placeholder="Configured" disabled={true} />
</div>
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button variant="secondary" type="button" onClick={e => onSecureJsonDataReset('accessKey')}>
Reset
</Button>
</div>
</div>
</div>
) : (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-14">Access Key ID</InlineFormLabel>
<div className="width-30">
<Input
className="width-30"
value={dataSourceConfig.secureJsonData?.accessKey || ''}
onChange={e => onSecureJsonDataChange('accessKey', e.currentTarget.value)}
/>
</div>
</div>
</div>
)}
{dataSourceConfig.secureJsonFields?.secretKey ? (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-14">Secret Access Key</InlineFormLabel>
<Input className="width-25" placeholder="Configured" disabled={true} />
</div>
<div className="gf-form">
<div className="max-width-30 gf-form-inline">
<Button variant="secondary" type="button" onClick={e => onSecureJsonDataReset('secretKey')}>
Reset
</Button>
</div>
</div>
</div>
) : (
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel className="width-14">Secret Access Key</InlineFormLabel>
<div className="width-30">
<Input
className="width-30"
value={dataSourceConfig.secureJsonData?.secretKey || ''}
onChange={e => onSecureJsonDataChange('secretKey', e.currentTarget.value)}
/>
</div>
</div>
</div>
)}
</div>
)}
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
className="width-14"
tooltip="ARN of the role to assume. Specifying a role here ensures that the selected authentication provider is used to assume the role rather than using the credentials directly. Leave blank if you don't need to assume a role."
>
Assume Role ARN
</InlineFormLabel>
<div className="width-30">
<Input
className="width-30"
placeholder="arn:aws:iam:*"
value={dataSourceConfig.jsonData.assumeRoleArn || ''}
onChange={e => onJsonDataChange('assumeRoleArn', e.currentTarget.value)}
/>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
className="width-14"
tooltip="If you are assuming a role in another account, that was created with an external ID, specify the external ID here."
>
External ID
</InlineFormLabel>
<div className="width-30">
<Input
className="width-30"
placeholder="External ID"
value={dataSourceConfig.jsonData.externalId || ''}
onChange={e => onJsonDataChange('externalId', e.currentTarget.value)}
/>
</div>
</div>
</div>
</div>
<div className="gf-form-inline">
<div className="gf-form">
<InlineFormLabel
className="width-14"
tooltip="Specify the region, for example, use ` us-west-2 ` for US West (Oregon)."
>
Default Region
</InlineFormLabel>
<Select
className="width-30"
value={regions.find(region => region.value === dataSourceConfig.jsonData.region)}
options={regions}
defaultValue={dataSourceConfig.jsonData.region || ''}
onChange={option => onJsonDataChange('region', option.value)}
/>
</div>
</div>
</div>
</>
);
};

View File

@@ -12,4 +12,6 @@ export interface HttpSettingsProps extends HttpSettingsBaseProps {
defaultUrl: string;
/** Show the http access help box */
showAccessOptions?: boolean;
/** Show the SigV4 auth toggle option */
sigV4AuthToggleEnabled?: boolean;
}