Skip to content

Commit

Permalink
feat(): support sigv4auth for prometheus metrics reader
Browse files Browse the repository at this point in the history
  • Loading branch information
ivyxjc committed Sep 14, 2023
1 parent bff2565 commit d21debd
Show file tree
Hide file tree
Showing 9 changed files with 352 additions and 1 deletion.
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ require (
github.com/HdrHistogram/hdrhistogram-go v1.1.2
github.com/Shopify/sarama v1.37.2
github.com/apache/thrift v0.19.0
github.com/aws/aws-sdk-go-v2 v1.21.0
github.com/aws/aws-sdk-go-v2/config v1.8.3
github.com/aws/aws-sdk-go-v2/credentials v1.13.35
github.com/aws/aws-sdk-go-v2/service/sts v1.21.5
github.com/bsm/sarama-cluster v2.1.13+incompatible
github.com/crossdock/crossdock-go v0.0.0-20160816171116-049aabb0122b
github.com/dgraph-io/badger/v3 v3.2103.5
Expand Down Expand Up @@ -76,6 +80,14 @@ require (
require (
github.com/VividCortex/gohistogram v1.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 // indirect
github.com/aws/smithy-go v1.14.2 // indirect
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
Expand Down
23 changes: 23 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,37 @@ github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:W
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc=
github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
github.com/aws/aws-sdk-go-v2/config v1.8.3 h1:o5583X4qUfuRrOGOgmOcDgvr5gJVSu57NK08cWAhIDk=
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
github.com/aws/aws-sdk-go-v2/credentials v1.13.35 h1:QpsNitYJu0GgvMBLUIYu9H4yryA5kMksjeIVQfgXrt8=
github.com/aws/aws-sdk-go-v2/credentials v1.13.35/go.mod h1:o7rCaLtvK0hUggAGclf76mNGGkaG5a9KWlp+d9IpcV8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 h1:leSJ6vCqtPpTmBIgE7044B1wql1E4n//McF+mEgNrYg=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o=
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
github.com/aws/aws-sdk-go-v2/service/sso v1.13.5 h1:oCvTFSDi67AX0pOX3PuPdGFewvLRU2zzFSrTsgURNo0=
github.com/aws/aws-sdk-go-v2/service/sso v1.13.5/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5 h1:dnInJb4S0oy8aQuri1mV6ipLlnZPfnsDNB9BGO9PDNY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.5/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4=
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
github.com/aws/aws-sdk-go-v2/service/sts v1.21.5 h1:CQBFElb0LS8RojMJlxRSo/HXipvTZW2S44Lt9Mk2aYQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.21.5/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU=
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ=
github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
Expand Down Expand Up @@ -315,6 +337,7 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
Expand Down
3 changes: 3 additions & 0 deletions pkg/prometheus/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (

// Configuration describes the options to customize the storage behavior.
type Configuration struct {
Authenticator string
Region string

ServerURL string
ConnectTimeout time.Duration
TLS tlscfg.Options
Expand Down
46 changes: 46 additions & 0 deletions pkg/sigv4authextension/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package sigv4authextension

import (
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"go.opentelemetry.io/collector/component"
)

// Config stores the configuration for the Sigv4 Authenticator
type Config struct {
Region string `mapstructure:"region,omitempty"`
Service string `mapstructure:"service,omitempty"`
AssumeRole AssumeRole `mapstructure:"assume_role"`
credsProvider *aws.CredentialsProvider
}

// AssumeRole holds the configuration needed to assume a role
type AssumeRole struct {
ARN string `mapstructure:"arn,omitempty"`
SessionName string `mapstructure:"session_name,omitempty"`
STSRegion string `mapstructure:"sts_region,omitempty"`
}

// compile time check that the Config struct satisfies the component.Config interface
var _ component.Config = (*Config)(nil)

// Validate checks that the configuration is valid.
// We aim to catch most errors here to ensure that we
// fail early and to avoid revalidating static data.
func (cfg *Config) Validate() error {
if cfg.AssumeRole.STSRegion == "" && cfg.Region != "" {
cfg.AssumeRole.STSRegion = cfg.Region
}

credsProvider, err := getCredsProviderFromConfig(cfg)
if err != nil {
return fmt.Errorf("could not retrieve credential provider: %w", err)
}
if credsProvider == nil {
return fmt.Errorf("credsProvider cannot be nil")
}
cfg.credsProvider = credsProvider

return nil
}
97 changes: 97 additions & 0 deletions pkg/sigv4authextension/extension.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package sigv4authextension

import (
"context"
"errors"
"fmt"
"github.com/aws/aws-sdk-go-v2/credentials"
"net/http"
"os"

"github.com/aws/aws-sdk-go-v2/aws"
sigv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
"go.opentelemetry.io/collector/component"
"go.opentelemetry.io/collector/extension/auth"
"go.uber.org/zap"
grpcCredentials "google.golang.org/grpc/credentials"
)

// Sigv4Auth is a struct that implements the auth.Client interface.
// It provides the implementation for providing Sigv4 authentication for HTTP requests only.
type Sigv4Auth struct {
cfg *Config
logger *zap.Logger
awsSDKInfo string
component.StartFunc // embedded default behavior to do nothing with Start()
component.ShutdownFunc // embedded default behavior to do nothing with Shutdown()
}

// compile time check that the Sigv4Auth struct satisfies the auth.Client interface
var _ auth.Client = (*Sigv4Auth)(nil)

// RoundTripper() returns a custom SigningRoundTripper.
func (sa *Sigv4Auth) RoundTripper(base http.RoundTripper) (http.RoundTripper, error) {
cfg := sa.cfg

signer := sigv4.NewSigner()

// Create the SigningRoundTripper struct
rt := SigningRoundTripper{
transport: base,
signer: signer,
region: cfg.Region,
service: cfg.Service,
credsProvider: cfg.credsProvider,
awsSDKInfo: sa.awsSDKInfo,
logger: sa.logger,
}

return &rt, nil
}

// PerRPCCredentials is implemented to satisfy the auth.Client interface but will not be implemented.
func (sa *Sigv4Auth) PerRPCCredentials() (grpcCredentials.PerRPCCredentials, error) {
return nil, errors.New("Not Implemented")
}

// newSigv4Extension() is called by createExtension() in factory.go and
// returns a new Sigv4Auth struct.
func NewSigv4Extension(cfg *Config, logger *zap.Logger) *Sigv4Auth {
awsSDKInfo := fmt.Sprintf("%s/%s", aws.SDKName, aws.SDKVersion)
return &Sigv4Auth{
cfg: cfg,
logger: logger,
awsSDKInfo: awsSDKInfo,
}
}

// getCredsProviderFromConfig() is a helper function that gets AWS credentials
// from the Config.
func getCredsProviderFromConfig(cfg *Config) (*aws.CredentialsProvider, error) {
awscfg, err := awsconfig.LoadDefaultConfig(context.Background(),
awsconfig.WithRegion(cfg.AssumeRole.STSRegion),
)
if err != nil {
return nil, err
}
if cfg.AssumeRole.ARN != "" {
stsSvc := sts.NewFromConfig(awscfg)

provider := stscreds.NewAssumeRoleProvider(stsSvc, cfg.AssumeRole.ARN)
awscfg.Credentials = aws.NewCredentialsCache(provider)
}
customAccessKey := os.Getenv("CUSTOM_AWS_ACCESS_KEY")
customSecretKey := os.Getenv("CUSTOM_AWS_SECRET_ACCESS_KEY")
if customAccessKey != "" && customSecretKey != "" {
awscfg.Credentials = credentials.NewStaticCredentialsProvider(customAccessKey, customSecretKey, "")
}
_, err = awscfg.Credentials.Retrieve(context.Background())
if err != nil {
return nil, err
}

return &awscfg.Credentials, nil
}
151 changes: 151 additions & 0 deletions pkg/sigv4authextension/signingroundtripper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package sigv4authextension

import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
sigv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"go.uber.org/zap"
)

var errNilRequest = errors.New("sigv4: unable to sign nil *http.Request")

// SigningRoundTripper is a custom RoundTripper that performs AWS Sigv4.
type SigningRoundTripper struct {
transport http.RoundTripper
signer *sigv4.Signer
region string
service string
credsProvider *aws.CredentialsProvider
awsSDKInfo string
logger *zap.Logger
}

// RoundTrip() executes a single HTTP transaction and returns an HTTP response, signing
// the request with Sigv4.
func (si *SigningRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req == nil {
si.logger.Warn("nil *http.Request encountered")
return nil, errNilRequest
}

req2, err := si.signRequest(req)
if err != nil {
si.logger.Debug("error signing request", zap.Error(err))
return nil, err
}

// Send the request
return si.transport.RoundTrip(req2)
}

func (si *SigningRoundTripper) signRequest(req *http.Request) (*http.Request, error) {
payloadHash, err := hashPayload(req)
if err != nil {
return nil, fmt.Errorf("unable to hash request body: %w", err)
}

// Clone request to ensure thread safety.
req2 := cloneRequest(req)

// Add the runtime information to the User-Agent header of the request
ua := req2.Header.Get("User-Agent")
if len(ua) > 0 {
ua = ua + " " + si.awsSDKInfo
} else {
ua = si.awsSDKInfo
}
req2.Header.Set("User-Agent", ua)

// Use user provided service/region if specified, use inferred service/region if not, then sign the request
service, region := si.inferServiceAndRegion(req2)
if si.credsProvider == nil {
return nil, fmt.Errorf("a credentials provider is not set")
}
creds, err := (*si.credsProvider).Retrieve(req2.Context())
if err != nil {
return nil, fmt.Errorf("error retrieving credentials: %w", err)
}

err = si.signer.SignHTTP(req.Context(), creds, req2, payloadHash, service, region, time.Now())
if err != nil {
return nil, fmt.Errorf("error signing the request: %w", err)
}

return req2, nil
}

// hashPayload creates a SHA256 hash of the request body
func hashPayload(req *http.Request) (string, error) {
if req.GetBody == nil {
// hash of an empty payload to use if there is no request body
return "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", nil
}

reqBody, err := req.GetBody()
if err != nil {
return "", err
}

// Hash the request body
h := sha256.New()
_, err = io.Copy(h, reqBody)
if err != nil {
return "", err
}

return hex.EncodeToString(h.Sum(nil)), reqBody.Close()
}

// inferServiceAndRegion attempts to infer a service
// and a region from an http.request, and returns either an empty
// string for both or a valid value for both.
func (si *SigningRoundTripper) inferServiceAndRegion(r *http.Request) (service string, region string) {
service = si.service
region = si.region

h := r.Host
if strings.HasPrefix(h, "aps-workspaces") {
if service == "" {
service = "aps"
}
rest := h[strings.Index(h, ".")+1:]
if region == "" {
region = rest[0:strings.Index(rest, ".")]
}
} else if strings.HasPrefix(h, "search-") {
if service == "" {
service = "es"
}
rest := h[strings.Index(h, ".")+1:]
if region == "" {
region = rest[0:strings.Index(rest, ".")]
}
}

if service == "" || region == "" {
si.logger.Warn("Unable to infer region and/or service from the URL. Please provide values for region and/or service in the collector configuration.")
}
return service, region
}

// cloneRequest() is a helper function that makes a shallow copy of the request and a
// deep copy of the header, for thread safety purposes.
func cloneRequest(r *http.Request) *http.Request {
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *r
// deep copy of the Header
r2.Header = make(http.Header, len(r.Header))
for k, s := range r.Header {
r2.Header[k] = append([]string(nil), s...)
}
return r2
}
Loading

0 comments on commit d21debd

Please sign in to comment.