mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
ab33e46789
commit
7d63b2c473
@ -280,10 +280,10 @@ editors_can_admin = false
|
||||
login_cookie_name = grafana_session
|
||||
|
||||
# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation (token_rotation_interval_minutes).
|
||||
login_maximum_inactive_lifetime_duration =
|
||||
login_maximum_inactive_lifetime_duration =
|
||||
|
||||
# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
|
||||
login_maximum_lifetime_duration =
|
||||
login_maximum_lifetime_duration =
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
token_rotation_interval_minutes = 10
|
||||
@ -307,6 +307,9 @@ oauth_state_cookie_max_age = 600
|
||||
# limit of api_key seconds to live before expiration
|
||||
api_key_max_seconds_to_live = -1
|
||||
|
||||
# Set to true to enable SigV4 authentication option for HTTP-based datasources
|
||||
sigv4_auth_enabled = false
|
||||
|
||||
#################################### Anonymous Auth ######################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
|
@ -278,11 +278,11 @@
|
||||
# Login cookie name
|
||||
;login_cookie_name = grafana_session
|
||||
|
||||
# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation
|
||||
;login_maximum_inactive_lifetime_duration =
|
||||
# The maximum lifetime (duration) an authenticated user can be inactive before being required to login at next visit. Default is 7 days (7d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month). The lifetime resets at each successful token rotation
|
||||
;login_maximum_inactive_lifetime_duration =
|
||||
|
||||
# The maximum lifetime (duration) an authenticated user can be logged in since login time before being required to login. Default is 30 days (30d). This setting should be expressed as a duration, e.g. 5m (minutes), 6h (hours), 10d (days), 2w (weeks), 1M (month).
|
||||
;login_maximum_lifetime_duration =
|
||||
;login_maximum_lifetime_duration =
|
||||
|
||||
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
|
||||
;token_rotation_interval_minutes = 10
|
||||
@ -306,6 +306,9 @@
|
||||
# limit of api_key seconds to live before expiration
|
||||
;api_key_max_seconds_to_live = -1
|
||||
|
||||
# Set to true to enable SigV4 authentication option for HTTP-based datasources.
|
||||
;sigv4_auth_enabled = false
|
||||
|
||||
#################################### Anonymous Auth ######################
|
||||
[auth.anonymous]
|
||||
# enable anonymous access
|
||||
@ -813,4 +816,4 @@
|
||||
;use_browser_locale = false
|
||||
|
||||
# Default timezone for user preferences. Options are 'browser' for the browser local timezone or a timezone name from IANA Time Zone database, e.g. 'UTC' or 'Europe/Amsterdam' etc.
|
||||
;default_timezone = browser
|
||||
;default_timezone = browser
|
||||
|
@ -649,6 +649,12 @@ Administrators can increase this if they experience OAuth login state mismatch e
|
||||
|
||||
Limit of API key seconds to live before expiration. Default is -1 (unlimited).
|
||||
|
||||
### sigv4_auth_enabled
|
||||
|
||||
> Only available in Grafana 7.3+.
|
||||
|
||||
Set to `true` to enable the AWS Signature Version 4 Authentication option for HTTP-based datasources. Default is `false`.
|
||||
|
||||
<hr />
|
||||
|
||||
## [auth.anonymous]
|
||||
|
@ -83,6 +83,7 @@ export interface GrafanaConfig {
|
||||
authProxyEnabled: boolean;
|
||||
exploreEnabled: boolean;
|
||||
ldapEnabled: boolean;
|
||||
sigV4AuthEnabled: boolean;
|
||||
samlEnabled: boolean;
|
||||
autoAssignOrg: boolean;
|
||||
verifyEmailEnabled: boolean;
|
||||
|
@ -36,6 +36,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
authProxyEnabled = false;
|
||||
exploreEnabled = false;
|
||||
ldapEnabled = false;
|
||||
sigV4AuthEnabled = false;
|
||||
samlEnabled = false;
|
||||
autoAssignOrg = true;
|
||||
verifyEmailEnabled = false;
|
||||
|
@ -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.*/}
|
||||
|
@ -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} />
|
||||
)}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -203,6 +203,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"alertingMinInterval": setting.AlertingMinInterval,
|
||||
"autoAssignOrg": setting.AutoAssignOrg,
|
||||
"verifyEmailEnabled": setting.VerifyEmailEnabled,
|
||||
"sigV4AuthEnabled": setting.SigV4AuthEnabled,
|
||||
"exploreEnabled": setting.ExploreEnabled,
|
||||
"googleAnalyticsId": setting.GoogleAnalyticsId,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
|
@ -69,6 +69,7 @@ type dataSourceTransport struct {
|
||||
datasourceName string
|
||||
headers map[string]string
|
||||
transport *http.Transport
|
||||
next http.RoundTripper
|
||||
}
|
||||
|
||||
func instrumentRoundtrip(datasourceName string, next http.RoundTripper) promhttp.RoundTripperFunc {
|
||||
@ -108,7 +109,7 @@ func (d *dataSourceTransport) RoundTrip(req *http.Request) (*http.Response, erro
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
return instrumentRoundtrip(d.datasourceName, d.transport).RoundTrip(req)
|
||||
return instrumentRoundtrip(d.datasourceName, d.next).RoundTrip(req)
|
||||
}
|
||||
|
||||
type cachedTransport struct {
|
||||
@ -133,6 +134,7 @@ func (ds *DataSource) GetHttpClient() (*http.Client, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Creates a HTTP Transport middleware chain
|
||||
func (ds *DataSource) GetHttpTransport() (*dataSourceTransport, error) {
|
||||
ptc.Lock()
|
||||
defer ptc.Unlock()
|
||||
@ -163,10 +165,19 @@ func (ds *DataSource) GetHttpTransport() (*dataSourceTransport, error) {
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
}
|
||||
|
||||
// Set default next round tripper to the default transport
|
||||
next := http.RoundTripper(transport)
|
||||
|
||||
// Add SigV4 middleware if enabled, which will then defer to the default transport
|
||||
if ds.JsonData != nil && ds.JsonData.Get("sigV4Auth").MustBool() && setting.SigV4AuthEnabled {
|
||||
next = ds.sigV4Middleware(transport)
|
||||
}
|
||||
|
||||
dsTransport := &dataSourceTransport{
|
||||
datasourceName: ds.Name,
|
||||
headers: customHeaders,
|
||||
transport: transport,
|
||||
datasourceName: ds.Name,
|
||||
next: next,
|
||||
}
|
||||
|
||||
ptc.cache[ds.Id] = cachedTransport{
|
||||
@ -177,6 +188,23 @@ func (ds *DataSource) GetHttpTransport() (*dataSourceTransport, error) {
|
||||
return dsTransport, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) sigV4Middleware(next http.RoundTripper) http.RoundTripper {
|
||||
decrypted := ds.DecryptedValues()
|
||||
|
||||
return &SigV4Middleware{
|
||||
Config: &Config{
|
||||
AccessKey: decrypted["accessKey"],
|
||||
SecretKey: decrypted["secretKey"],
|
||||
Region: ds.JsonData.Get("region").MustString(),
|
||||
AssumeRoleARN: ds.JsonData.Get("assumeRoleArn").MustString(),
|
||||
AuthType: ds.JsonData.Get("authType").MustString(),
|
||||
ExternalID: ds.JsonData.Get("externalId").MustString(),
|
||||
Profile: ds.JsonData.Get("profile").MustString(),
|
||||
},
|
||||
Next: next,
|
||||
}
|
||||
}
|
||||
|
||||
func (ds *DataSource) GetTLSConfig() (*tls.Config, error) {
|
||||
var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool
|
||||
if ds.JsonData != nil {
|
||||
|
@ -291,6 +291,78 @@ func TestDataSourceDecryptionCache(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDataSourceSigV4Auth(t *testing.T) {
|
||||
Convey("When caching a datasource proxy with middleware", t, func() {
|
||||
clearDSProxyCache()
|
||||
origEnabled := setting.SigV4AuthEnabled
|
||||
setting.SigV4AuthEnabled = true
|
||||
t.Cleanup(func() {
|
||||
setting.SigV4AuthEnabled = origEnabled
|
||||
})
|
||||
|
||||
json, err := simplejson.NewJson([]byte(`{ "sigV4Auth": true }`))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ds := DataSource{
|
||||
JsonData: json,
|
||||
}
|
||||
|
||||
t, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("SigV4 is in middleware chain if configured in JsonData", func() {
|
||||
m1, ok := t.next.(*SigV4Middleware)
|
||||
So(ok, ShouldEqual, true)
|
||||
|
||||
_, ok = m1.Next.(*http.Transport)
|
||||
So(ok, ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy with middleware", t, func() {
|
||||
clearDSProxyCache()
|
||||
origEnabled := setting.SigV4AuthEnabled
|
||||
setting.SigV4AuthEnabled = true
|
||||
t.Cleanup(func() {
|
||||
setting.SigV4AuthEnabled = origEnabled
|
||||
})
|
||||
|
||||
ds := DataSource{}
|
||||
|
||||
t, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should not include sigV4 middleware if not configured in JsonData", func() {
|
||||
_, ok := t.next.(*http.Transport)
|
||||
So(ok, ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When caching a datasource proxy with middleware", t, func() {
|
||||
clearDSProxyCache()
|
||||
origEnabled := setting.SigV4AuthEnabled
|
||||
setting.SigV4AuthEnabled = false
|
||||
t.Cleanup(func() {
|
||||
setting.SigV4AuthEnabled = origEnabled
|
||||
})
|
||||
|
||||
json, err := simplejson.NewJson([]byte(`{ "sigV4Auth": true }`))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
ds := DataSource{
|
||||
JsonData: json,
|
||||
}
|
||||
|
||||
t, err := ds.GetHttpTransport()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should not include sigV4 middleware if not configured in app config", func() {
|
||||
_, ok := t.next.(*http.Transport)
|
||||
So(ok, ShouldEqual, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func clearDSProxyCache() {
|
||||
ptc.Lock()
|
||||
defer ptc.Unlock()
|
||||
|
110
pkg/models/sigv4.go
Normal file
110
pkg/models/sigv4.go
Normal file
@ -0,0 +1,110 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
||||
)
|
||||
|
||||
type AuthType string
|
||||
|
||||
const (
|
||||
Default AuthType = "default"
|
||||
Keys AuthType = "keys"
|
||||
Credentials AuthType = "credentials"
|
||||
)
|
||||
|
||||
type SigV4Middleware struct {
|
||||
Config *Config
|
||||
Next http.RoundTripper
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
AuthType string
|
||||
|
||||
Profile string
|
||||
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
|
||||
AssumeRoleARN string
|
||||
ExternalID string
|
||||
Region string
|
||||
}
|
||||
|
||||
func (m *SigV4Middleware) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
_, err := m.signRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m.Next == nil {
|
||||
return http.DefaultTransport.RoundTrip(req)
|
||||
}
|
||||
|
||||
return m.Next.RoundTrip(req)
|
||||
}
|
||||
|
||||
func (m *SigV4Middleware) signRequest(req *http.Request) (http.Header, error) {
|
||||
signer, err := m.signer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Body != nil {
|
||||
// consume entire request body so that the signer can generate a hash from the contents
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return signer.Sign(req, bytes.NewReader(body), "grafana", m.Config.Region, time.Now().UTC())
|
||||
}
|
||||
return signer.Sign(req, nil, "grafana", m.Config.Region, time.Now().UTC())
|
||||
}
|
||||
|
||||
func (m *SigV4Middleware) signer() (*v4.Signer, error) {
|
||||
c, err := m.credentials()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m.Config.AssumeRoleARN != "" {
|
||||
s, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(m.Config.Region),
|
||||
Credentials: c},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v4.NewSigner(stscreds.NewCredentials(s, m.Config.AssumeRoleARN)), nil
|
||||
}
|
||||
|
||||
return v4.NewSigner(c), nil
|
||||
}
|
||||
|
||||
func (m *SigV4Middleware) credentials() (*credentials.Credentials, error) {
|
||||
authType := AuthType(m.Config.AuthType)
|
||||
|
||||
switch authType {
|
||||
case Default:
|
||||
return defaults.CredChain(defaults.Config(), defaults.Handlers()), nil
|
||||
case Keys:
|
||||
return credentials.NewStaticCredentials(m.Config.AccessKey, m.Config.SecretKey, ""), nil
|
||||
case Credentials:
|
||||
return credentials.NewSharedCredentials("", m.Config.Profile), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unrecognized authType: %s", authType)
|
||||
}
|
@ -143,6 +143,7 @@ var (
|
||||
AdminPassword string
|
||||
LoginCookieName string
|
||||
LoginMaxLifetime time.Duration
|
||||
SigV4AuthEnabled bool
|
||||
|
||||
AnonymousEnabled bool
|
||||
AnonymousOrgName string
|
||||
@ -280,6 +281,7 @@ type Cfg struct {
|
||||
LoginMaxInactiveLifetime time.Duration
|
||||
LoginMaxLifetime time.Duration
|
||||
TokenRotationIntervalMinutes int
|
||||
SigV4AuthEnabled bool
|
||||
|
||||
// OAuth
|
||||
OAuthCookieMaxAge int
|
||||
@ -990,6 +992,10 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
|
||||
cfg.OAuthCookieMaxAge = auth.Key("oauth_state_cookie_max_age").MustInt(600)
|
||||
SignoutRedirectUrl = valueAsString(auth, "signout_redirect_url", "")
|
||||
|
||||
// SigV4
|
||||
SigV4AuthEnabled = auth.Key("sigv4_auth_enabled").MustBool(false)
|
||||
cfg.SigV4AuthEnabled = SigV4AuthEnabled
|
||||
|
||||
// SAML auth
|
||||
cfg.SAMLEnabled = iniFile.Section("auth.saml").Key("enabled").MustBool(false)
|
||||
|
||||
|
@ -41,7 +41,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
this.loadRegionsPromise = makePromiseCancelable(this.loadRegions());
|
||||
this.loadRegionsPromise.promise.catch(({ isCanceled }) => {
|
||||
if (isCanceled) {
|
||||
console.warn('Cloud Watch ConfigEditor has unmounted, intialization was canceled');
|
||||
console.warn('Cloud Watch ConfigEditor has unmounted, initialization was canceled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { ElasticsearchOptions } from '../types';
|
||||
import { defaultMaxConcurrentShardRequests, ElasticDetails } from './ElasticDetails';
|
||||
import { LogsConfig } from './LogsConfig';
|
||||
import { DataLinks } from './DataLinks';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>;
|
||||
export const ConfigEditor = (props: Props) => {
|
||||
@ -34,6 +35,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
dataSourceConfig={options}
|
||||
showAccessOptions={true}
|
||||
onChange={onOptionsChange}
|
||||
sigV4AuthToggleEnabled={config.sigV4AuthEnabled}
|
||||
/>
|
||||
|
||||
<ElasticDetails value={options} onChange={onOptionsChange} />
|
||||
|
@ -3,6 +3,7 @@ import { DataSourceHttpSettings } from '@grafana/ui';
|
||||
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
||||
import { PromSettings } from './PromSettings';
|
||||
import { PromOptions } from '../types';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
export type Props = DataSourcePluginOptionsEditorProps<PromOptions>;
|
||||
export const ConfigEditor = (props: Props) => {
|
||||
@ -14,6 +15,7 @@ export const ConfigEditor = (props: Props) => {
|
||||
dataSourceConfig={options}
|
||||
showAccessOptions={true}
|
||||
onChange={onOptionsChange}
|
||||
sigV4AuthToggleEnabled={config.sigV4AuthEnabled}
|
||||
/>
|
||||
|
||||
<PromSettings options={options} onOptionsChange={onOptionsChange} />
|
||||
|
Loading…
Reference in New Issue
Block a user