forked from jaegertracing/jaeger
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(): support sigv4auth for prometheus metrics reader
- Loading branch information
Showing
9 changed files
with
352 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.