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
co-authored by Arve Knudsen
parent ab33e46789
commit 7d63b2c473
17 changed files with 517 additions and 11 deletions
+30 -2
View File
@@ -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 {
+72
View File
@@ -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
View 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)
}