From 4d2b20f7270d392ad007e9615c5d9256ea60f8f3 Mon Sep 17 00:00:00 2001 From: Will Browne Date: Mon, 26 Oct 2020 12:36:11 +0100 Subject: [PATCH] Auth: Fix SigV4 request verification step for Amazon Elasticsearch Service (#28481) * remove forward header and add extra steps for encoding * add comment * re-use forwarded header * fix service param * Make SDK the default auth option in UI * use SDK code instead * propagate err * refactor flow * fix prom service namespace --- .../DataSourceHttpSettings.mdx | 2 +- .../DataSourceSettings/SigV4AuthSettings.tsx | 2 +- pkg/models/datasource_cache.go | 15 +++-- pkg/models/sigv4.go | 66 +++++++++++++++---- 4 files changed, 62 insertions(+), 23 deletions(-) diff --git a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx index 7bb270bdab1..36e355f06f3 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx +++ b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.mdx @@ -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, config } = props; + const { options, onOptionsChange } = props; return ( <> = props => { authProvider => authProvider.value === dataSourceConfig.jsonData.sigV4AuthType )} options={authProviderOptions} - defaultValue={dataSourceConfig.jsonData.sigV4AuthType || 'keys'} + defaultValue={dataSourceConfig.jsonData.sigV4AuthType || authProviderOptions[0]} onChange={option => { onJsonDataChange('sigV4AuthType', option.value); }} diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go index 38470174279..39b8434ae3b 100644 --- a/pkg/models/datasource_cache.go +++ b/pkg/models/datasource_cache.go @@ -193,13 +193,14 @@ func (ds *DataSource) sigV4Middleware(next http.RoundTripper) http.RoundTripper return &SigV4Middleware{ Config: &Config{ - AccessKey: decrypted["sigV4AccessKey"], - SecretKey: decrypted["sigV4SecretKey"], - Region: ds.JsonData.Get("sigV4Region").MustString(), - AssumeRoleARN: ds.JsonData.Get("sigV4AssumeRoleArn").MustString(), - AuthType: ds.JsonData.Get("sigV4AuthType").MustString(), - ExternalID: ds.JsonData.Get("sigV4ExternalId").MustString(), - Profile: ds.JsonData.Get("sigV4Profile").MustString(), + DatasourceType: ds.Type, + AccessKey: decrypted["sigV4AccessKey"], + SecretKey: decrypted["sigV4SecretKey"], + Region: ds.JsonData.Get("sigV4Region").MustString(), + AssumeRoleARN: ds.JsonData.Get("sigV4AssumeRoleArn").MustString(), + AuthType: ds.JsonData.Get("sigV4AuthType").MustString(), + ExternalID: ds.JsonData.Get("sigV4ExternalId").MustString(), + Profile: ds.JsonData.Get("sigV4Profile").MustString(), }, Next: next, } diff --git a/pkg/models/sigv4.go b/pkg/models/sigv4.go index 10751870efc..72fad32cbf9 100644 --- a/pkg/models/sigv4.go +++ b/pkg/models/sigv4.go @@ -5,17 +5,16 @@ import ( "fmt" "io/ioutil" "net/http" + "strings" "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" + "github.com/aws/aws-sdk-go/aws/credentials/stscreds" + "github.com/aws/aws-sdk-go/aws/defaults" + "github.com/aws/aws-sdk-go/aws/session" v4 "github.com/aws/aws-sdk-go/aws/signer/v4" + "github.com/aws/aws-sdk-go/private/protocol/rest" ) type AuthType string @@ -36,6 +35,8 @@ type Config struct { Profile string + DatasourceType string + AccessKey string SecretKey string @@ -63,15 +64,29 @@ func (m *SigV4Middleware) signRequest(req *http.Request) (http.Header, error) { 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()) + body, err := replaceBody(req) + if err != nil { + return nil, err } - return signer.Sign(req, nil, "grafana", m.Config.Region, time.Now().UTC()) + + if strings.Contains(req.URL.RawPath, "%2C") { + req.URL.RawPath = rest.EscapePath(req.URL.RawPath, false) + } + + // if X-Forwarded-For header is present, omit during signing step as it breaks AWS request verification + forwardHeader := req.Header.Get("X-Forwarded-For") + if forwardHeader != "" { + req.Header.Del("X-Forwarded-For") + + header, err := signer.Sign(req, bytes.NewReader(body), awsServiceNamespace(m.Config.DatasourceType), m.Config.Region, time.Now().UTC()) + + // reset pre-existing X-Forwarded-For header value + req.Header.Set("X-Forwarded-For", forwardHeader) + + return header, err + } + + return signer.Sign(req, bytes.NewReader(body), awsServiceNamespace(m.Config.DatasourceType), m.Config.Region, time.Now().UTC()) } func (m *SigV4Middleware) signer() (*v4.Signer, error) { @@ -108,3 +123,26 @@ func (m *SigV4Middleware) credentials() (*credentials.Credentials, error) { return nil, fmt.Errorf("unrecognized authType: %s", authType) } + +func replaceBody(req *http.Request) ([]byte, error) { + if req.Body == nil { + return []byte{}, nil + } + payload, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + req.Body = ioutil.NopCloser(bytes.NewReader(payload)) + return payload, nil +} + +func awsServiceNamespace(dsType string) string { + switch dsType { + case DS_ES: + return "es" + case DS_PROMETHEUS: + return "aps" + default: + panic(fmt.Sprintf("Unsupported datasource %s", dsType)) + } +}