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:
co-authored by
Arve Knudsen
parent
ab33e46789
commit
7d63b2c473
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user