build(deps): bump github.com/open-policy-agent/opa from 0.51.0 to 0.59.0
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.51.0 to 0.59.0. - [Release notes](https://github.com/open-policy-agent/opa/releases) - [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-policy-agent/opa/compare/v0.51.0...v0.59.0) --- updated-dependencies: - dependency-name: github.com/open-policy-agent/opa dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
committed by
Ralf Haferkamp
parent
a6a6c22c14
commit
1f069c7c00
+915
@@ -0,0 +1,915 @@
|
||||
// Copyright 2019 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jwa"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jws"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jws/sign"
|
||||
"github.com/open-policy-agent/opa/internal/providers/aws"
|
||||
"github.com/open-policy-agent/opa/internal/uuid"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default to s3 when the service for sigv4 signing is not specified for backwards compatibility
|
||||
awsSigv4SigningDefaultService = "s3"
|
||||
)
|
||||
|
||||
// DefaultTLSConfig defines standard TLS configurations based on the Config
|
||||
func DefaultTLSConfig(c Config) (*tls.Config, error) {
|
||||
t := &tls.Config{}
|
||||
url, err := url.Parse(c.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if url.Scheme == "https" {
|
||||
t.InsecureSkipVerify = c.AllowInsecureTLS
|
||||
}
|
||||
|
||||
if c.TLS != nil && c.TLS.CACert != "" {
|
||||
caCert, err := os.ReadFile(c.TLS.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rootCAs *x509.CertPool
|
||||
if c.TLS.SystemCARequired {
|
||||
rootCAs, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
ok := rootCAs.AppendCertsFromPEM(caCert)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to parse and append CA certificate to certificate pool")
|
||||
}
|
||||
t.RootCAs = rootCAs
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DefaultRoundTripperClient is a reasonable set of defaults for HTTP auth plugins
|
||||
func DefaultRoundTripperClient(t *tls.Config, timeout int64) *http.Client {
|
||||
// Ensure we use a http.Transport with proper settings: the zero values are not
|
||||
// a good choice, as they cause leaking connections:
|
||||
// https://github.com/golang/go/issues/19620
|
||||
|
||||
// copy, we don't want to alter the default client's Transport
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.ResponseHeaderTimeout = time.Duration(timeout) * time.Second
|
||||
tr.TLSClientConfig = t
|
||||
|
||||
c := *http.DefaultClient
|
||||
c.Transport = tr
|
||||
return &c
|
||||
}
|
||||
|
||||
// defaultAuthPlugin represents baseline 'no auth' behavior if no alternative plugin is specified for a service
|
||||
type defaultAuthPlugin struct{}
|
||||
|
||||
func (*defaultAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (*defaultAuthPlugin) Prepare(*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type serverTLSConfig struct {
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
SystemCARequired bool `json:"system_ca_required,omitempty"`
|
||||
}
|
||||
|
||||
// bearerAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
|
||||
type bearerAuthPlugin struct {
|
||||
Token string `json:"token"`
|
||||
TokenPath string `json:"token_path"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
|
||||
// encode is set to true for the OCIDownloader because
|
||||
// it expects tokens in plain text but needs them in base64.
|
||||
encode bool
|
||||
}
|
||||
|
||||
func (ap *bearerAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.Token != "" && ap.TokenPath != "" {
|
||||
return nil, errors.New("invalid config: specify a value for either the \"token\" or \"token_path\" field")
|
||||
}
|
||||
|
||||
if ap.Scheme == "" {
|
||||
ap.Scheme = "Bearer"
|
||||
}
|
||||
|
||||
if c.Type == "oci" {
|
||||
// Standard rest clients use the bearer token as it is defined in the Config
|
||||
// but the OCIDownloader needs it encoded to base64 before using to sign a request.
|
||||
ap.encode = true
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *bearerAuthPlugin) Prepare(req *http.Request) error {
|
||||
token := ap.Token
|
||||
|
||||
if ap.TokenPath != "" {
|
||||
bytes, err := os.ReadFile(ap.TokenPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token = strings.TrimSpace(string(bytes))
|
||||
}
|
||||
|
||||
if ap.encode {
|
||||
token = base64.StdEncoding.EncodeToString([]byte(token))
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
|
||||
return nil
|
||||
}
|
||||
|
||||
type tokenEndpointResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
type awsKmsKeyConfig struct {
|
||||
Name string `json:"name"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
func convertSignatureToBase64(alg string, der []byte) (string, error) {
|
||||
r, s, derErr := pointsFromDER(der)
|
||||
if derErr != nil {
|
||||
return "", fmt.Errorf("failed to read points from der %v", derErr)
|
||||
}
|
||||
|
||||
signatureData, err := convertPointsToBase64(alg, r.Bytes(), s.Bytes())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return signatureData, nil
|
||||
}
|
||||
|
||||
func pointsFromDER(der []byte) (R, S *big.Int, err error) {
|
||||
R, S = &big.Int{}, &big.Int{}
|
||||
data := asn1.RawValue{}
|
||||
if _, err := asn1.Unmarshal(der, &data); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshall the signature from DER format %v", err)
|
||||
|
||||
}
|
||||
// https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_ResponseSyntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3
|
||||
// The format of our DER string is 0x02 + rlen + r + 0x02 + slen + s
|
||||
rLen := data.Bytes[1] // The entire length of R + offset of 2 for 0x02 and rlen
|
||||
r := data.Bytes[2 : rLen+2]
|
||||
// Ignore the next 0x02 and slen bytes and just take the start of S to the end of the byte array
|
||||
s := data.Bytes[rLen+4:]
|
||||
R.SetBytes(r)
|
||||
S.SetBytes(s)
|
||||
return
|
||||
}
|
||||
|
||||
func convertPointsToBase64(alg string, r, s []byte) (string, error) {
|
||||
curveBits, err := retrieveCurveBits(alg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keyBytes := curveBits / 8
|
||||
if curveBits%8 > 0 {
|
||||
keyBytes++
|
||||
}
|
||||
// We serialize the outputs (r and s) into big-endian byte arrays and pad
|
||||
// them with zeros on the left to make sure the sizes work out. Both arrays
|
||||
// must be keyBytes long, and the output must be 2*keyBytes long.
|
||||
rBytesPadded := make([]byte, keyBytes)
|
||||
copy(rBytesPadded[keyBytes-len(r):], r)
|
||||
sBytesPadded := make([]byte, keyBytes)
|
||||
copy(sBytesPadded[keyBytes-len(s):], s)
|
||||
signatureEnc := append(rBytesPadded, sBytesPadded...)
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(signatureEnc), nil
|
||||
}
|
||||
|
||||
func retrieveCurveBits(alg string) (int, error) {
|
||||
var curveBits int
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
curveBits = 256
|
||||
case "ECDSA_SHA_384":
|
||||
curveBits = 384
|
||||
case "ECDSA_SHA_512":
|
||||
curveBits = 512
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
return curveBits, nil
|
||||
}
|
||||
|
||||
func messageDigest(message []byte, alg string) ([]byte, error) {
|
||||
var digest hash.Hash
|
||||
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
digest = sha256.New()
|
||||
case "ECDSA_SHA_384":
|
||||
digest = sha512.New384()
|
||||
case "ECDSA_SHA_512":
|
||||
digest = sha512.New()
|
||||
default:
|
||||
return []byte{}, fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
|
||||
digest.Write(message)
|
||||
return digest.Sum(nil), nil
|
||||
}
|
||||
|
||||
// oauth2ClientCredentialsAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
|
||||
// obtained through the OAuth2 client credentials flow
|
||||
type oauth2ClientCredentialsAuthPlugin struct {
|
||||
GrantType string `json:"grant_type"`
|
||||
TokenURL string `json:"token_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
SigningKeyID string `json:"signing_key"`
|
||||
Thumbprint string `json:"thumbprint"`
|
||||
Claims map[string]interface{} `json:"additional_claims"`
|
||||
IncludeJti bool `json:"include_jti_claim"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
AdditionalHeaders map[string]string `json:"additional_headers,omitempty"`
|
||||
AdditionalParameters map[string]string `json:"additional_parameters,omitempty"`
|
||||
AWSKmsKey *awsKmsKeyConfig `json:"aws_kms,omitempty"`
|
||||
AWSSigningPlugin *awsSigningAuthPlugin `json:"aws_signing,omitempty"`
|
||||
|
||||
signingKey *keys.Config
|
||||
signingKeyParsed interface{}
|
||||
tokenCache *oauth2Token
|
||||
tlsSkipVerify bool
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type oauth2Token struct {
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(ctx context.Context, claims map[string]interface{}, signingKey interface{}) (*string, error) {
|
||||
now := time.Now()
|
||||
baseClaims := map[string]interface{}{
|
||||
"iat": now.Unix(),
|
||||
"exp": now.Add(10 * time.Minute).Unix(),
|
||||
}
|
||||
if claims == nil {
|
||||
claims = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range baseClaims {
|
||||
claims[k] = v
|
||||
}
|
||||
|
||||
if len(ap.Scopes) > 0 {
|
||||
claims["scope"] = strings.Join(ap.Scopes, " ")
|
||||
}
|
||||
|
||||
if ap.IncludeJti {
|
||||
jti, err := uuid.New(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims["jti"] = jti
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jwsHeaders []byte
|
||||
var signatureAlg string
|
||||
if ap.AWSKmsKey == nil {
|
||||
signatureAlg = ap.signingKey.Algorithm
|
||||
} else {
|
||||
signatureAlg, err = ap.mapKMSAlgToSign(ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if ap.Thumbprint != "" {
|
||||
bytes, err := hex.DecodeString(ap.Thumbprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x5t := base64.URLEncoding.EncodeToString(bytes)
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s","x5t":"%s"}`, signatureAlg, x5t))
|
||||
} else {
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s"}`, signatureAlg))
|
||||
}
|
||||
var jwsCompact []byte
|
||||
if ap.AWSKmsKey == nil {
|
||||
jwsCompact, err = jws.SignLiteral(payload,
|
||||
jwa.SignatureAlgorithm(signatureAlg),
|
||||
signingKey,
|
||||
jwsHeaders,
|
||||
rand.Reader)
|
||||
} else {
|
||||
jwsCompact, err = ap.SignWithKMS(ctx, payload, jwsHeaders)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jwt := string(jwsCompact)
|
||||
|
||||
return &jwt, nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) mapKMSAlgToSign(alg string) (string, error) {
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
return "ES256", nil
|
||||
case "ECDSA_SHA_384":
|
||||
return "ES384", nil
|
||||
case "ECDSA_SHA_512":
|
||||
return "ES512", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
}
|
||||
|
||||
// SignWithKMS will sign the JWT in AWS using the key stored in the supplied kmsArn
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) SignWithKMS(ctx context.Context, payload []byte, hdrBuf []byte) ([]byte, error) {
|
||||
|
||||
encodedHdr := base64.RawURLEncoding.EncodeToString(hdrBuf)
|
||||
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
|
||||
input := strings.Join(
|
||||
[]string{
|
||||
encodedHdr,
|
||||
encodedPayload,
|
||||
}, ".",
|
||||
)
|
||||
digest, err := messageDigest([]byte(input), ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ap.AWSSigningPlugin != nil {
|
||||
signature, err := ap.AWSSigningPlugin.SignDigest(ctx, digest, ap.AWSKmsKey.Name, ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatureData, err := convertSignatureToBase64(ap.AWSKmsKey.Algorithm, der)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedAssertion := input + "." + signatureData
|
||||
|
||||
return []byte(signedAssertion), nil
|
||||
}
|
||||
return nil, errors.New("missing AWS credentials, failed to sign the assertion with kms")
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) parseSigningKey(c Config) (err error) {
|
||||
if ap.SigningKeyID == "" {
|
||||
return errors.New("signing_key required for jwt_bearer grant type")
|
||||
}
|
||||
|
||||
if val, ok := c.keys[ap.SigningKeyID]; ok {
|
||||
if val.PrivateKey == "" {
|
||||
return errors.New("referenced signing_key does not include a private key")
|
||||
}
|
||||
ap.signingKey = val
|
||||
} else {
|
||||
return errors.New("signing_key refers to non-existent key")
|
||||
}
|
||||
|
||||
alg := jwa.SignatureAlgorithm(ap.signingKey.Algorithm)
|
||||
ap.signingKeyParsed, err = sign.GetSigningKey(ap.signingKey.PrivateKey, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.GrantType == "" {
|
||||
// Use client_credentials as default to not break existing config
|
||||
ap.GrantType = grantTypeClientCredentials
|
||||
} else if ap.GrantType != grantTypeClientCredentials && ap.GrantType != grantTypeJwtBearer {
|
||||
return nil, errors.New("grant_type must be either client_credentials or jwt_bearer")
|
||||
}
|
||||
|
||||
if ap.GrantType == grantTypeJwtBearer || (ap.GrantType == grantTypeClientCredentials && ap.SigningKeyID != "") {
|
||||
if err = ap.parseSigningKey(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit skip verify from the "parent" settings. Should this be configurable on the credentials too?
|
||||
ap.tlsSkipVerify = c.AllowInsecureTLS
|
||||
|
||||
ap.logger = c.logger
|
||||
|
||||
if !strings.HasPrefix(ap.TokenURL, "https://") {
|
||||
return nil, errors.New("token_url required to use https scheme")
|
||||
}
|
||||
if ap.GrantType == grantTypeClientCredentials {
|
||||
if ap.AWSKmsKey != nil && (ap.ClientSecret != "" || ap.SigningKeyID != "") ||
|
||||
(ap.ClientSecret != "" && ap.SigningKeyID != "") {
|
||||
return nil, errors.New("can only use one of client_secret, signing_key or signing_kms_key for client_credentials")
|
||||
}
|
||||
if ap.SigningKeyID == "" && ap.AWSKmsKey == nil && (ap.ClientID == "" || ap.ClientSecret == "") {
|
||||
return nil, errors.New("client_id and client_secret required")
|
||||
}
|
||||
if ap.AWSKmsKey != nil {
|
||||
if ap.AWSSigningPlugin == nil {
|
||||
return nil, errors.New("aws_kms and aws_signing required")
|
||||
}
|
||||
// initialize the awsSigningAuthPlugin
|
||||
_, err = ap.AWSSigningPlugin.NewClient(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
// requestToken tries to obtain an access token using either the client credentials flow
|
||||
// https://tools.ietf.org/html/rfc6749#section-4.4
|
||||
// or the JWT authorization grant
|
||||
// https://tools.ietf.org/html/rfc7523
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) requestToken(ctx context.Context) (*oauth2Token, error) {
|
||||
body := url.Values{}
|
||||
if ap.GrantType == grantTypeJwtBearer {
|
||||
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
|
||||
body.Add("assertion", *authJwt)
|
||||
} else {
|
||||
body.Add("grant_type", grantTypeClientCredentials)
|
||||
|
||||
if ap.SigningKeyID != "" || ap.AWSKmsKey != nil {
|
||||
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
|
||||
body.Add("client_assertion", *authJwt)
|
||||
|
||||
if ap.ClientID != "" {
|
||||
body.Add("client_id", ap.ClientID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ap.Scopes) > 0 {
|
||||
body.Add("scope", strings.Join(ap.Scopes, " "))
|
||||
}
|
||||
|
||||
for k, v := range ap.AdditionalParameters {
|
||||
body.Set(k, v)
|
||||
}
|
||||
|
||||
r, err := http.NewRequestWithContext(ctx, "POST", ap.TokenURL, strings.NewReader(body.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
if ap.GrantType == grantTypeClientCredentials && ap.ClientSecret != "" {
|
||||
r.SetBasicAuth(ap.ClientID, ap.ClientSecret)
|
||||
}
|
||||
|
||||
for k, v := range ap.AdditionalHeaders {
|
||||
r.Header.Add(k, v)
|
||||
}
|
||||
|
||||
client := DefaultRoundTripperClient(&tls.Config{InsecureSkipVerify: ap.tlsSkipVerify}, 10)
|
||||
response, err := client.Do(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyRaw, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("error in response from OAuth2 token endpoint: %v", string(bodyRaw))
|
||||
}
|
||||
|
||||
var tokenResponse tokenEndpointResponse
|
||||
err = json.Unmarshal(bodyRaw, &tokenResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.ToLower(tokenResponse.TokenType) != "bearer" {
|
||||
return nil, errors.New("unknown token type returned from token endpoint")
|
||||
}
|
||||
|
||||
return &oauth2Token{
|
||||
Token: strings.TrimSpace(tokenResponse.AccessToken),
|
||||
ExpiresAt: time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) Prepare(req *http.Request) error {
|
||||
minTokenLifetime := float64(10)
|
||||
if ap.tokenCache == nil || time.Until(ap.tokenCache.ExpiresAt).Seconds() < minTokenLifetime {
|
||||
ap.logger.Debug("Requesting token from token_url %v", ap.TokenURL)
|
||||
token, err := ap.requestToken(req.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ap.tokenCache = token
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", ap.tokenCache.Token))
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientTLSAuthPlugin represents authentication via client certificate on a TLS connection
|
||||
type clientTLSAuthPlugin struct {
|
||||
Cert string `json:"cert"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"` // Deprecated: Use `services[_].tls.ca_cert` instead
|
||||
SystemCARequired bool `json:"system_ca_required,omitempty"` // Deprecated: Use `services[_].tls.system_ca_required` instead
|
||||
}
|
||||
|
||||
func (ap *clientTLSAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
tlsConfig, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.Cert == "" {
|
||||
return nil, errors.New("client certificate is needed when client TLS is enabled")
|
||||
}
|
||||
if ap.PrivateKey == "" {
|
||||
return nil, errors.New("private key is needed when client TLS is enabled")
|
||||
}
|
||||
|
||||
var keyPEMBlock []byte
|
||||
data, err := os.ReadFile(ap.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, errors.New("PEM data could not be found")
|
||||
}
|
||||
|
||||
// nolint: staticcheck // We don't want to forbid users from using this encryption.
|
||||
if x509.IsEncryptedPEMBlock(block) {
|
||||
if ap.PrivateKeyPassphrase == "" {
|
||||
return nil, errors.New("client certificate passphrase is needed, because the certificate is password encrypted")
|
||||
}
|
||||
// nolint: staticcheck // We don't want to forbid users from using this encryption.
|
||||
block, err := x509.DecryptPEMBlock(block, []byte(ap.PrivateKeyPassphrase))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block)
|
||||
if err != nil {
|
||||
key, err = x509.ParsePKCS1PrivateKey(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err)
|
||||
}
|
||||
}
|
||||
rsa, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("private key is invalid")
|
||||
}
|
||||
keyPEMBlock = pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(rsa),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
keyPEMBlock = data
|
||||
}
|
||||
|
||||
certPEMBlock, err := os.ReadFile(ap.Cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
|
||||
var client *http.Client
|
||||
|
||||
if c.TLS != nil && c.TLS.CACert != "" {
|
||||
client = DefaultRoundTripperClient(tlsConfig, *c.ResponseHeaderTimeoutSeconds)
|
||||
} else {
|
||||
if ap.CACert != "" {
|
||||
c.logger.Warn("Deprecated 'services[_].credentials.client_tls.ca_cert' configuration specified. Use 'services[_].tls.ca_cert' instead. See https://www.openpolicyagent.org/docs/latest/configuration/#services")
|
||||
caCert, err := os.ReadFile(ap.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var caCertPool *x509.CertPool
|
||||
if ap.SystemCARequired {
|
||||
caCertPool, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
caCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
ok := caCertPool.AppendCertsFromPEM(caCert)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to parse and append CA certificate to certificate pool")
|
||||
}
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
client = DefaultRoundTripperClient(tlsConfig, *c.ResponseHeaderTimeoutSeconds)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (ap *clientTLSAuthPlugin) Prepare(req *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// awsSigningAuthPlugin represents authentication using AWS V4 HMAC signing in the Authorization header
|
||||
type awsSigningAuthPlugin struct {
|
||||
AWSEnvironmentCredentials *awsEnvironmentCredentialService `json:"environment_credentials,omitempty"`
|
||||
AWSMetadataCredentials *awsMetadataCredentialService `json:"metadata_credentials,omitempty"`
|
||||
AWSWebIdentityCredentials *awsWebIdentityCredentialService `json:"web_identity_credentials,omitempty"`
|
||||
AWSProfileCredentials *awsProfileCredentialService `json:"profile_credentials,omitempty"`
|
||||
|
||||
AWSService string `json:"service,omitempty"`
|
||||
AWSSignatureVersion string `json:"signature_version,omitempty"`
|
||||
|
||||
ecrAuthPlugin *ecrAuthPlugin
|
||||
kmsSignPlugin *awsKMSSignPlugin
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type awsCredentialServiceChain struct {
|
||||
awsCredentialServices []awsCredentialService
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (acs *awsCredentialServiceChain) addService(service awsCredentialService) {
|
||||
acs.awsCredentialServices = append(acs.awsCredentialServices, service)
|
||||
}
|
||||
|
||||
type awsCredentialCheckErrors []*awsCredentialCheckError
|
||||
|
||||
func (e awsCredentialCheckErrors) Error() string {
|
||||
|
||||
if len(e) == 0 {
|
||||
return "no error(s)"
|
||||
}
|
||||
|
||||
if len(e) == 1 {
|
||||
return fmt.Sprintf("1 error occurred: %v", e[0].Error())
|
||||
}
|
||||
|
||||
s := make([]string, len(e))
|
||||
for i, err := range e {
|
||||
s[i] = err.Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d errors occurred:\n%s", len(e), strings.Join(s, "\n"))
|
||||
}
|
||||
|
||||
type awsCredentialCheckError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func newAWSCredentialError(message string) *awsCredentialCheckError {
|
||||
return &awsCredentialCheckError{
|
||||
message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *awsCredentialCheckError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (acs *awsCredentialServiceChain) credentials(ctx context.Context) (aws.Credentials, error) {
|
||||
var errs awsCredentialCheckErrors
|
||||
|
||||
for _, service := range acs.awsCredentialServices {
|
||||
credential, err := service.credentials(ctx)
|
||||
if err != nil {
|
||||
acs.logger.Debug("awsSigningAuthPlugin:%T failed: %v", service, err)
|
||||
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return aws.Credentials{}, err
|
||||
}
|
||||
|
||||
errs = append(errs, newAWSCredentialError(err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
acs.logger.Debug("awsSigningAuthPlugin:%T successful", service)
|
||||
return credential, nil
|
||||
}
|
||||
|
||||
return aws.Credentials{}, fmt.Errorf("all AWS credential providers failed: %v", errs)
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) awsCredentialService() awsCredentialService {
|
||||
chain := awsCredentialServiceChain{
|
||||
logger: ap.logger,
|
||||
}
|
||||
|
||||
/*
|
||||
Here we maintain the order of addition to the chain inline with
|
||||
the order of credential providers followed by default by the
|
||||
AWS SDK. For example
|
||||
|
||||
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
|
||||
*/
|
||||
|
||||
if ap.AWSEnvironmentCredentials != nil {
|
||||
ap.AWSEnvironmentCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSEnvironmentCredentials)
|
||||
}
|
||||
|
||||
if ap.AWSWebIdentityCredentials != nil {
|
||||
ap.AWSWebIdentityCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSWebIdentityCredentials)
|
||||
}
|
||||
|
||||
if ap.AWSProfileCredentials != nil {
|
||||
ap.AWSProfileCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSProfileCredentials)
|
||||
}
|
||||
|
||||
if ap.AWSMetadataCredentials != nil {
|
||||
ap.AWSMetadataCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSMetadataCredentials)
|
||||
}
|
||||
|
||||
return &chain
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.logger == nil {
|
||||
ap.logger = c.logger
|
||||
}
|
||||
|
||||
if err := ap.validateAndSetDefaults(c.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) Prepare(req *http.Request) error {
|
||||
switch ap.AWSService {
|
||||
case "ecr":
|
||||
return ap.ecrAuthPlugin.Prepare(req)
|
||||
default:
|
||||
creds, err := ap.awsCredentialService().credentials(req.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
ap.logger.Debug("Signing request with AWS credentials.")
|
||||
|
||||
return aws.SignRequest(req, ap.AWSService, creds, time.Now(), ap.AWSSignatureVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error {
|
||||
cfgs := map[bool]int{}
|
||||
cfgs[ap.AWSEnvironmentCredentials != nil]++
|
||||
cfgs[ap.AWSMetadataCredentials != nil]++
|
||||
cfgs[ap.AWSWebIdentityCredentials != nil]++
|
||||
cfgs[ap.AWSProfileCredentials != nil]++
|
||||
|
||||
if cfgs[true] == 0 {
|
||||
return errors.New("a AWS credential service must be specified when S3 signing is enabled")
|
||||
}
|
||||
|
||||
if ap.AWSMetadataCredentials != nil {
|
||||
if ap.AWSMetadataCredentials.RegionName == "" {
|
||||
return errors.New("at least aws_region must be specified for AWS metadata credential service")
|
||||
}
|
||||
}
|
||||
|
||||
if ap.AWSWebIdentityCredentials != nil {
|
||||
if err := ap.AWSWebIdentityCredentials.populateFromEnv(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ap.AWSService = strings.ToLower(ap.AWSService)
|
||||
|
||||
// Only allow ECR for OCI service types
|
||||
if serviceType == "oci" {
|
||||
if ap.AWSService == "" {
|
||||
ap.AWSService = "ecr"
|
||||
}
|
||||
|
||||
if ap.AWSService != "ecr" {
|
||||
return fmt.Errorf(`cannot use aws service %q with service type "oci"`, ap.AWSService)
|
||||
}
|
||||
|
||||
// We need to setup a special auth plugin for ECR.
|
||||
ap.ecrAuthPlugin = newECRAuthPlugin(ap)
|
||||
} else {
|
||||
// Disallow ECR for non-OCI service types
|
||||
if ap.AWSService == "ecr" {
|
||||
return errors.New(`aws service "ecr" must be used with service type "oci"`)
|
||||
}
|
||||
if ap.AWSService == "kms" && ap.kmsSignPlugin == nil {
|
||||
// We need a special plugin for KMS.
|
||||
ap.kmsSignPlugin = newKMSSignPlugin(ap)
|
||||
}
|
||||
if ap.AWSService == "" {
|
||||
ap.AWSService = awsSigv4SigningDefaultService
|
||||
}
|
||||
}
|
||||
|
||||
if ap.AWSSignatureVersion == "" {
|
||||
ap.AWSSignatureVersion = "4"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
|
||||
switch ap.AWSService {
|
||||
case "kms":
|
||||
return ap.kmsSignPlugin.SignDigest(ctx, digest, keyID, signingAlgorithm)
|
||||
default:
|
||||
return "", fmt.Errorf(`cannot use SignDigest with aws service %q`, ap.AWSService)
|
||||
}
|
||||
}
|
||||
+557
@@ -0,0 +1,557 @@
|
||||
// Copyright 2019 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
"github.com/open-policy-agent/opa/internal/providers/aws"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||
ec2DefaultCredServicePath = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
|
||||
ec2DefaultTokenPath = "http://169.254.169.254/latest/api/token"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html
|
||||
ecsDefaultCredServicePath = "http://169.254.170.2"
|
||||
ecsRelativePathEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
|
||||
stsDefaultDomain = "amazonaws.com"
|
||||
stsDefaultPath = "https://sts.%s"
|
||||
stsRegionPath = "https://sts.%s.%s"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
|
||||
accessKeyEnvVar = "AWS_ACCESS_KEY_ID"
|
||||
secretKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
|
||||
securityTokenEnvVar = "AWS_SECURITY_TOKEN"
|
||||
sessionTokenEnvVar = "AWS_SESSION_TOKEN"
|
||||
awsRegionEnvVar = "AWS_REGION"
|
||||
awsDomainEnvVar = "AWS_DOMAIN"
|
||||
awsRoleArnEnvVar = "AWS_ROLE_ARN"
|
||||
awsWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
|
||||
awsCredentialsFileEnvVar = "AWS_SHARED_CREDENTIALS_FILE"
|
||||
awsProfileEnvVar = "AWS_PROFILE"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/sdkref/latest/guide/settings-global.html
|
||||
accessKeyGlobalSetting = "aws_access_key_id"
|
||||
secretKeyGlobalSetting = "aws_secret_access_key"
|
||||
securityTokenGlobalSetting = "aws_session_token"
|
||||
)
|
||||
|
||||
// awsCredentialService represents the interface for AWS credential providers
|
||||
type awsCredentialService interface {
|
||||
credentials(context.Context) (aws.Credentials, error)
|
||||
}
|
||||
|
||||
// awsEnvironmentCredentialService represents an static environment-variable credential provider for AWS
|
||||
type awsEnvironmentCredentialService struct {
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsEnvironmentCredentialService) credentials(context.Context) (aws.Credentials, error) {
|
||||
var creds aws.Credentials
|
||||
creds.AccessKey = os.Getenv(accessKeyEnvVar)
|
||||
if creds.AccessKey == "" {
|
||||
return creds, errors.New("no " + accessKeyEnvVar + " set in environment")
|
||||
}
|
||||
creds.SecretKey = os.Getenv(secretKeyEnvVar)
|
||||
if creds.SecretKey == "" {
|
||||
return creds, errors.New("no " + secretKeyEnvVar + " set in environment")
|
||||
}
|
||||
creds.RegionName = os.Getenv(awsRegionEnvVar)
|
||||
if creds.RegionName == "" {
|
||||
return creds, errors.New("no " + awsRegionEnvVar + " set in environment")
|
||||
}
|
||||
// SessionToken is required if using temporary ENV credentials from assumed IAM role
|
||||
// Missing SessionToken results with 403 s3 error.
|
||||
creds.SessionToken = os.Getenv(sessionTokenEnvVar)
|
||||
if creds.SessionToken == "" {
|
||||
// In case of missing SessionToken try to get SecurityToken
|
||||
// AWS switched to use SessionToken, but SecurityToken was left for backward compatibility
|
||||
creds.SessionToken = os.Getenv(securityTokenEnvVar)
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// awsProfileCredentialService represents a credential provider for AWS that extracts credentials from the AWS
|
||||
// credentials file
|
||||
type awsProfileCredentialService struct {
|
||||
|
||||
// Path to the credentials file.
|
||||
//
|
||||
// If empty will look for "AWS_SHARED_CREDENTIALS_FILE" env variable. If the
|
||||
// env value is empty will default to current user's home directory.
|
||||
// Linux/OSX: "$HOME/.aws/credentials"
|
||||
// Windows: "%USERPROFILE%\.aws\credentials"
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// AWS Profile to extract credentials from the credentials file. If empty
|
||||
// will default to environment variable "AWS_PROFILE" or "default" if
|
||||
// environment variable is also not set.
|
||||
Profile string `json:"profile,omitempty"`
|
||||
|
||||
RegionName string `json:"aws_region"`
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsProfileCredentialService) credentials(context.Context) (aws.Credentials, error) {
|
||||
var creds aws.Credentials
|
||||
|
||||
filename, err := cs.path()
|
||||
if err != nil {
|
||||
return creds, err
|
||||
}
|
||||
|
||||
cfg, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return creds, fmt.Errorf("failed to read credentials file: %v", err)
|
||||
}
|
||||
|
||||
profile, err := cfg.GetSection(cs.profile())
|
||||
if err != nil {
|
||||
return creds, fmt.Errorf("failed to get profile: %v", err)
|
||||
}
|
||||
|
||||
creds.AccessKey = profile.Key(accessKeyGlobalSetting).String()
|
||||
if creds.AccessKey == "" {
|
||||
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, accessKeyGlobalSetting)
|
||||
}
|
||||
|
||||
creds.SecretKey = profile.Key(secretKeyGlobalSetting).String()
|
||||
if creds.SecretKey == "" {
|
||||
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, secretKeyGlobalSetting)
|
||||
}
|
||||
|
||||
creds.SessionToken = profile.Key(securityTokenGlobalSetting).String() // default to empty string
|
||||
|
||||
if cs.RegionName == "" {
|
||||
if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" {
|
||||
return creds, errors.New("no " + awsRegionEnvVar + " set in environment or configuration")
|
||||
}
|
||||
}
|
||||
creds.RegionName = cs.RegionName
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func (cs *awsProfileCredentialService) path() (string, error) {
|
||||
if len(cs.Path) != 0 {
|
||||
return cs.Path, nil
|
||||
}
|
||||
|
||||
if cs.Path = os.Getenv(awsCredentialsFileEnvVar); len(cs.Path) != 0 {
|
||||
return cs.Path, nil
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("user home directory not found: %w", err)
|
||||
}
|
||||
|
||||
cs.Path = filepath.Join(homeDir, ".aws", "credentials")
|
||||
|
||||
return cs.Path, nil
|
||||
}
|
||||
|
||||
func (cs *awsProfileCredentialService) profile() string {
|
||||
if cs.Profile != "" {
|
||||
return cs.Profile
|
||||
}
|
||||
|
||||
cs.Profile = os.Getenv(awsProfileEnvVar)
|
||||
|
||||
if cs.Profile == "" {
|
||||
cs.Profile = "default"
|
||||
}
|
||||
|
||||
return cs.Profile
|
||||
}
|
||||
|
||||
// awsMetadataCredentialService represents an EC2 metadata service credential provider for AWS
|
||||
type awsMetadataCredentialService struct {
|
||||
RoleName string `json:"iam_role,omitempty"`
|
||||
RegionName string `json:"aws_region"`
|
||||
creds aws.Credentials
|
||||
expiration time.Time
|
||||
credServicePath string
|
||||
tokenPath string
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) urlForMetadataService() (string, error) {
|
||||
// override default path for testing
|
||||
if cs.credServicePath != "" {
|
||||
return cs.credServicePath + cs.RoleName, nil
|
||||
}
|
||||
// otherwise, normal flow
|
||||
// if a role name is provided, look up via the EC2 credential service
|
||||
if cs.RoleName != "" {
|
||||
return ec2DefaultCredServicePath + cs.RoleName, nil
|
||||
}
|
||||
// otherwise, check environment to see if it looks like we're in an ECS
|
||||
// container (with implied role association)
|
||||
if isECS() {
|
||||
return ecsDefaultCredServicePath + os.Getenv(ecsRelativePathEnvVar), nil
|
||||
}
|
||||
// if there's no role name and we don't appear to have a path to the
|
||||
// ECS container service, then the configuration is invalid
|
||||
return "", errors.New("metadata endpoint cannot be determined from settings and environment")
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) tokenRequest(ctx context.Context) (*http.Request, error) {
|
||||
tokenURL := ec2DefaultTokenPath
|
||||
if cs.tokenPath != "" {
|
||||
// override for testing
|
||||
tokenURL = cs.tokenPath
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, tokenURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we are going to use the token in the immediate future, so a long TTL is not necessary
|
||||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) refreshFromService(ctx context.Context) error {
|
||||
// define the expected JSON payload from the EC2 credential service
|
||||
// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||
type metadataPayload struct {
|
||||
Code string
|
||||
AccessKeyID string `json:"AccessKeyId"`
|
||||
SecretAccessKey string
|
||||
Token string
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// Short circuit if a reasonable amount of time until credential expiration remains
|
||||
const tokenExpirationMargin = 5 * time.Minute
|
||||
|
||||
if time.Now().Add(tokenExpirationMargin).Before(cs.expiration) {
|
||||
cs.logger.Debug("Credentials previously obtained from metadata service still valid.")
|
||||
return nil
|
||||
}
|
||||
|
||||
cs.logger.Debug("Obtaining credentials from metadata service.")
|
||||
metaDataURL, err := cs.urlForMetadataService()
|
||||
if err != nil {
|
||||
// configuration issue or missing ECS environment
|
||||
return err
|
||||
}
|
||||
|
||||
// construct an HTTP client with a reasonably short timeout
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, metaDataURL, nil)
|
||||
if err != nil {
|
||||
return errors.New("unable to construct metadata HTTP request: " + err.Error())
|
||||
}
|
||||
|
||||
// if in the EC2 environment, we will use IMDSv2, which requires a session cookie from a
|
||||
// PUT request on the token endpoint before it will give the credentials, this provides
|
||||
// protection from SSRF attacks
|
||||
if !isECS() {
|
||||
tokenReq, err := cs.tokenRequest(ctx)
|
||||
if err != nil {
|
||||
return errors.New("unable to construct metadata token HTTP request: " + err.Error())
|
||||
}
|
||||
body, err := aws.DoRequestWithClient(tokenReq, client, "metadata token", cs.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// token is the body of response; add to header of metadata request
|
||||
req.Header.Set("X-aws-ec2-metadata-token", string(body))
|
||||
}
|
||||
|
||||
body, err := aws.DoRequestWithClient(req, client, "metadata", cs.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload metadataPayload
|
||||
err = json.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
return errors.New("failed to parse credential response from metadata service: " + err.Error())
|
||||
}
|
||||
|
||||
// Only the EC2 endpoint returns the "Code" element which indicates whether the query was
|
||||
// successful; the ECS endpoint does not! Some other fields are missing in the ECS payload
|
||||
// but we do not depend on them.
|
||||
if cs.RoleName != "" && payload.Code != "Success" {
|
||||
return errors.New("metadata service query did not succeed: " + payload.Code)
|
||||
}
|
||||
|
||||
cs.expiration = payload.Expiration
|
||||
cs.creds.AccessKey = payload.AccessKeyID
|
||||
cs.creds.SecretKey = payload.SecretAccessKey
|
||||
cs.creds.SessionToken = payload.Token
|
||||
cs.creds.RegionName = cs.RegionName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) credentials(ctx context.Context) (aws.Credentials, error) {
|
||||
err := cs.refreshFromService(ctx)
|
||||
if err != nil {
|
||||
return cs.creds, err
|
||||
}
|
||||
return cs.creds, nil
|
||||
}
|
||||
|
||||
// awsWebIdentityCredentialService represents an STS WebIdentity credential services
|
||||
type awsWebIdentityCredentialService struct {
|
||||
RoleArn string
|
||||
WebIdentityTokenFile string
|
||||
RegionName string `json:"aws_region"`
|
||||
SessionName string `json:"session_name"`
|
||||
Domain string `json:"aws_domain"`
|
||||
stsURL string
|
||||
creds aws.Credentials
|
||||
expiration time.Time
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) populateFromEnv() error {
|
||||
cs.RoleArn = os.Getenv(awsRoleArnEnvVar)
|
||||
if cs.RoleArn == "" {
|
||||
return errors.New("no " + awsRoleArnEnvVar + " set in environment")
|
||||
}
|
||||
cs.WebIdentityTokenFile = os.Getenv(awsWebIdentityTokenFileEnvVar)
|
||||
if cs.WebIdentityTokenFile == "" {
|
||||
return errors.New("no " + awsWebIdentityTokenFileEnvVar + " set in environment")
|
||||
}
|
||||
|
||||
if cs.Domain == "" {
|
||||
cs.Domain = os.Getenv(awsDomainEnvVar)
|
||||
}
|
||||
|
||||
if cs.RegionName == "" {
|
||||
if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" {
|
||||
return errors.New("no " + awsRegionEnvVar + " set in environment or configuration")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) stsPath() string {
|
||||
var domain string
|
||||
if cs.Domain != "" {
|
||||
domain = strings.ToLower(cs.Domain)
|
||||
} else {
|
||||
domain = stsDefaultDomain
|
||||
}
|
||||
|
||||
var stsPath string
|
||||
switch {
|
||||
case cs.stsURL != "":
|
||||
stsPath = cs.stsURL
|
||||
case cs.RegionName != "":
|
||||
stsPath = fmt.Sprintf(stsRegionPath, strings.ToLower(cs.RegionName), domain)
|
||||
default:
|
||||
stsPath = fmt.Sprintf(stsDefaultPath, domain)
|
||||
}
|
||||
return stsPath
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) refreshFromService(ctx context.Context) error {
|
||||
// define the expected JSON payload from the EC2 credential service
|
||||
// ref. https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
|
||||
type responsePayload struct {
|
||||
Result struct {
|
||||
Credentials struct {
|
||||
SessionToken string
|
||||
SecretAccessKey string
|
||||
Expiration time.Time
|
||||
AccessKeyID string `xml:"AccessKeyId"`
|
||||
}
|
||||
} `xml:"AssumeRoleWithWebIdentityResult"`
|
||||
}
|
||||
|
||||
// short circuit if a reasonable amount of time until credential expiration remains
|
||||
if time.Now().Add(time.Minute * 5).Before(cs.expiration) {
|
||||
cs.logger.Debug("Credentials previously obtained from sts service still valid.")
|
||||
return nil
|
||||
}
|
||||
|
||||
cs.logger.Debug("Obtaining credentials from sts for role %s.", cs.RoleArn)
|
||||
|
||||
var sessionName string
|
||||
if cs.SessionName == "" {
|
||||
sessionName = "open-policy-agent"
|
||||
} else {
|
||||
sessionName = cs.SessionName
|
||||
}
|
||||
|
||||
tokenData, err := os.ReadFile(cs.WebIdentityTokenFile)
|
||||
if err != nil {
|
||||
return errors.New("unable to read web token for sts HTTP request: " + err.Error())
|
||||
}
|
||||
|
||||
token := string(tokenData)
|
||||
|
||||
queryVals := url.Values{
|
||||
"Action": []string{"AssumeRoleWithWebIdentity"},
|
||||
"RoleSessionName": []string{sessionName},
|
||||
"RoleArn": []string{cs.RoleArn},
|
||||
"WebIdentityToken": []string{token},
|
||||
"Version": []string{"2011-06-15"},
|
||||
}
|
||||
stsRequestURL, _ := url.Parse(cs.stsPath())
|
||||
|
||||
// construct an HTTP client with a reasonably short timeout
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, stsRequestURL.String(), strings.NewReader(queryVals.Encode()))
|
||||
if err != nil {
|
||||
return errors.New("unable to construct STS HTTP request: " + err.Error())
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
body, err := aws.DoRequestWithClient(req, client, "STS", cs.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload responsePayload
|
||||
err = xml.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
return errors.New("failed to parse credential response from STS service: " + err.Error())
|
||||
}
|
||||
|
||||
cs.expiration = payload.Result.Credentials.Expiration
|
||||
cs.creds.AccessKey = payload.Result.Credentials.AccessKeyID
|
||||
cs.creds.SecretKey = payload.Result.Credentials.SecretAccessKey
|
||||
cs.creds.SessionToken = payload.Result.Credentials.SessionToken
|
||||
cs.creds.RegionName = cs.RegionName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) credentials(ctx context.Context) (aws.Credentials, error) {
|
||||
err := cs.refreshFromService(ctx)
|
||||
if err != nil {
|
||||
return cs.creds, err
|
||||
}
|
||||
return cs.creds, nil
|
||||
}
|
||||
|
||||
func isECS() bool {
|
||||
// the special relative path URI is set by the container agent in the ECS environment only
|
||||
_, isECS := os.LookupEnv(ecsRelativePathEnvVar)
|
||||
return isECS
|
||||
}
|
||||
|
||||
// ecrAuthPlugin authorizes requests to AWS ECR.
|
||||
type ecrAuthPlugin struct {
|
||||
token aws.ECRAuthorizationToken
|
||||
|
||||
// awsAuthPlugin is used to sign ecr authorization token requests.
|
||||
awsAuthPlugin *awsSigningAuthPlugin
|
||||
|
||||
// ecr represents the service we request tokens from.
|
||||
ecr ecr
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type ecr interface {
|
||||
GetAuthorizationToken(context.Context, aws.Credentials, string) (aws.ECRAuthorizationToken, error)
|
||||
}
|
||||
|
||||
func newECRAuthPlugin(ap *awsSigningAuthPlugin) *ecrAuthPlugin {
|
||||
return &ecrAuthPlugin{
|
||||
awsAuthPlugin: ap,
|
||||
ecr: aws.NewECR(ap.logger),
|
||||
logger: ap.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare should be called with any request to AWS ECR.
|
||||
// It takes care of retrieving an ECR authorization token to sign
|
||||
// the request with.
|
||||
func (ap *ecrAuthPlugin) Prepare(r *http.Request) error {
|
||||
if !ap.token.IsValid() {
|
||||
ap.logger.Debug("Refreshing ECR auth token")
|
||||
if err := ap.refreshAuthorizationToken(r.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ap.logger.Debug("Signing request with ECR authorization token")
|
||||
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Basic %s", ap.token.AuthorizationToken))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *ecrAuthPlugin) refreshAuthorizationToken(ctx context.Context) error {
|
||||
creds, err := ap.awsAuthPlugin.awsCredentialService().credentials(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
token, err := ap.ecr.GetAuthorizationToken(ctx, creds, ap.awsAuthPlugin.AWSSignatureVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ecr: failed to get authorization token: %w", err)
|
||||
}
|
||||
|
||||
ap.token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// awsKMSSignPlugin signs digests using AWS KMS.
|
||||
type awsKMSSignPlugin struct {
|
||||
|
||||
// awsAuthPlugin is used to sign kms sign requests.
|
||||
awsAuthPlugin *awsSigningAuthPlugin
|
||||
|
||||
// kms represents the service for signing digests.
|
||||
kms awskms
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type awskms interface {
|
||||
SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string, creds aws.Credentials, signatureVersion string) (string, error)
|
||||
}
|
||||
|
||||
func newKMSSignPlugin(ap *awsSigningAuthPlugin) *awsKMSSignPlugin {
|
||||
return &awsKMSSignPlugin{
|
||||
awsAuthPlugin: ap,
|
||||
kms: aws.NewKMS(ap.logger),
|
||||
logger: ap.logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *awsKMSSignPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
|
||||
creds, err := ap.awsAuthPlugin.awsCredentialService().credentials(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
signature, err := ap.kms.SignDigest(ctx, digest, keyID, signingAlgorithm, creds, ap.awsAuthPlugin.AWSSignatureVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("kms: failed to sign digest: %w", err)
|
||||
}
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||
defaultAPIVersion = "2018-02-01"
|
||||
defaultResource = "https://storage.azure.com/"
|
||||
timeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// azureManagedIdentitiesToken holds a token for managed identities for Azure resources
|
||||
type azureManagedIdentitiesToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
ExpiresOn string `json:"expires_on"`
|
||||
NotBefore string `json:"not_before"`
|
||||
Resource string `json:"resource"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// azureManagedIdentitiesError represents an error fetching an azureManagedIdentitiesToken
|
||||
type azureManagedIdentitiesError struct {
|
||||
Err string `json:"error"`
|
||||
Description string `json:"error_description"`
|
||||
Endpoint string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (e *azureManagedIdentitiesError) Error() string {
|
||||
return fmt.Sprintf("%v %s retrieving azure token from %s: %s", e.StatusCode, e.Err, e.Endpoint, e.Description)
|
||||
}
|
||||
|
||||
// azureManagedIdentitiesAuthPlugin uses an azureManagedIdentitiesToken.AccessToken for bearer authorization
|
||||
type azureManagedIdentitiesAuthPlugin struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
APIVersion string `json:"api_version"`
|
||||
Resource string `json:"resource"`
|
||||
ObjectID string `json:"object_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
MiResID string `json:"mi_res_id"`
|
||||
}
|
||||
|
||||
func (ap *azureManagedIdentitiesAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
if c.Type == "oci" {
|
||||
return nil, errors.New("azure managed identities auth: OCI service not supported")
|
||||
}
|
||||
|
||||
if ap.Endpoint == "" {
|
||||
ap.Endpoint = azureIMDSEndpoint
|
||||
}
|
||||
|
||||
if ap.Resource == "" {
|
||||
ap.Resource = defaultResource
|
||||
}
|
||||
|
||||
if ap.APIVersion == "" {
|
||||
ap.APIVersion = defaultAPIVersion
|
||||
}
|
||||
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *azureManagedIdentitiesAuthPlugin) Prepare(req *http.Request) error {
|
||||
token, err := azureManagedIdentitiesTokenRequest(
|
||||
ap.Endpoint, ap.APIVersion, ap.Resource,
|
||||
ap.ObjectID, ap.ClientID, ap.MiResID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// azureManagedIdentitiesTokenRequest fetches an azureManagedIdentitiesToken
|
||||
func azureManagedIdentitiesTokenRequest(
|
||||
endpoint, apiVersion, resource, objectID, clientID, miResID string,
|
||||
) (azureManagedIdentitiesToken, error) {
|
||||
var token azureManagedIdentitiesToken
|
||||
e := buildAzureManagedIdentitiesRequestPath(endpoint, apiVersion, resource, objectID, clientID, miResID)
|
||||
|
||||
request, err := http.NewRequest("GET", e, nil)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
request.Header.Add("Metadata", "true")
|
||||
|
||||
httpClient := http.Client{Timeout: timeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
if s := response.StatusCode; s != http.StatusOK {
|
||||
var azureError azureManagedIdentitiesError
|
||||
err = json.Unmarshal(data, &azureError)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
azureError.Endpoint = e
|
||||
azureError.StatusCode = s
|
||||
return token, &azureError
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &token)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// buildAzureManagedIdentitiesRequestPath constructs the request URL for an Azure managed identities token request
|
||||
func buildAzureManagedIdentitiesRequestPath(
|
||||
endpoint, apiVersion, resource, objectID, clientID, miResID string,
|
||||
) string {
|
||||
params := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
"resource": []string{resource},
|
||||
}
|
||||
|
||||
if objectID != "" {
|
||||
params.Add("object_id", objectID)
|
||||
}
|
||||
|
||||
if clientID != "" {
|
||||
params.Add("client_id", clientID)
|
||||
}
|
||||
|
||||
if miResID != "" {
|
||||
params.Add("mi_res_id", miResID)
|
||||
}
|
||||
|
||||
return endpoint + "?" + params.Encode()
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// Copyright 2020 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultGCPMetadataEndpoint = "http://metadata.google.internal"
|
||||
defaultAccessTokenPath = "/computeMetadata/v1/instance/service-accounts/default/token"
|
||||
defaultIdentityTokenPath = "/computeMetadata/v1/instance/service-accounts/default/identity"
|
||||
)
|
||||
|
||||
// AccessToken holds a GCP access token.
|
||||
type AccessToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
type gcpMetadataError struct {
|
||||
err error
|
||||
endpoint string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (e *gcpMetadataError) Error() string {
|
||||
return fmt.Sprintf("error retrieving gcp ID token from %s %d: %v", e.endpoint, e.statusCode, e.err)
|
||||
}
|
||||
|
||||
func (e *gcpMetadataError) Unwrap() error { return e.err }
|
||||
|
||||
var (
|
||||
errGCPMetadataNotFound = errors.New("not found")
|
||||
errGCPMetadataInvalidRequest = errors.New("invalid request")
|
||||
errGCPMetadataUnexpected = errors.New("unexpected error")
|
||||
)
|
||||
|
||||
// gcpMetadataAuthPlugin represents authentication via GCP metadata service.
|
||||
type gcpMetadataAuthPlugin struct {
|
||||
AccessTokenPath string `json:"access_token_path"`
|
||||
Audience string `json:"audience"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
IdentityTokenPath string `json:"identity_token_path"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
func (ap *gcpMetadataAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
if ap.Audience == "" && len(ap.Scopes) == 0 {
|
||||
return nil, errors.New("audience or scopes is required when gcp metadata is enabled")
|
||||
}
|
||||
|
||||
if ap.Audience != "" && len(ap.Scopes) > 0 {
|
||||
return nil, errors.New("either audience or scopes can be set, not both, when gcp metadata is enabled")
|
||||
}
|
||||
|
||||
if ap.Endpoint == "" {
|
||||
ap.Endpoint = defaultGCPMetadataEndpoint
|
||||
}
|
||||
|
||||
if ap.AccessTokenPath == "" {
|
||||
ap.AccessTokenPath = defaultAccessTokenPath
|
||||
}
|
||||
|
||||
if ap.IdentityTokenPath == "" {
|
||||
ap.IdentityTokenPath = defaultIdentityTokenPath
|
||||
}
|
||||
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *gcpMetadataAuthPlugin) Prepare(req *http.Request) error {
|
||||
var err error
|
||||
var token string
|
||||
|
||||
if ap.Audience != "" {
|
||||
token, err = identityTokenFromMetadataService(ap.Endpoint, ap.IdentityTokenPath, ap.Audience)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving identity token from gcp metadata service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ap.Scopes) != 0 {
|
||||
token, err = accessTokenFromMetadataService(ap.Endpoint, ap.AccessTokenPath, ap.Scopes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving access token from gcp metadata service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token))
|
||||
return nil
|
||||
}
|
||||
|
||||
// accessTokenFromMetadataService returns an access token based on the scopes.
|
||||
func accessTokenFromMetadataService(endpoint, path string, scopes []string) (string, error) {
|
||||
s := strings.Join(scopes, ",")
|
||||
|
||||
e := fmt.Sprintf("%s%s?scopes=%s", endpoint, path, s)
|
||||
|
||||
data, err := gcpMetadataServiceRequest(e)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var accessToken AccessToken
|
||||
err = json.Unmarshal(data, &accessToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return accessToken.AccessToken, nil
|
||||
}
|
||||
|
||||
// identityTokenFromMetadataService returns an identity token based on the audience.
|
||||
func identityTokenFromMetadataService(endpoint, path, audience string) (string, error) {
|
||||
e := fmt.Sprintf("%s%s?audience=%s", endpoint, path, audience)
|
||||
|
||||
data, err := gcpMetadataServiceRequest(e)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func gcpMetadataServiceRequest(endpoint string) ([]byte, error) {
|
||||
request, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Add("Metadata-Flavor", "Google")
|
||||
|
||||
timeout := time.Duration(5) * time.Second
|
||||
httpClient := http.Client{Timeout: timeout}
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch s := response.StatusCode; s {
|
||||
case 200:
|
||||
break
|
||||
case 400:
|
||||
return nil, &gcpMetadataError{errGCPMetadataInvalidRequest, endpoint, s}
|
||||
case 404:
|
||||
return nil, &gcpMetadataError{errGCPMetadataNotFound, endpoint, s}
|
||||
default:
|
||||
return nil, &gcpMetadataError{errGCPMetadataUnexpected, endpoint, s}
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
// Copyright 2018 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package rest implements a REST client for communicating with remote services.
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/version"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
"github.com/open-policy-agent/opa/tracing"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultResponseHeaderTimeoutSeconds = int64(10)
|
||||
defaultResponseSizeLimitBytes = 1024
|
||||
|
||||
grantTypeClientCredentials = "client_credentials"
|
||||
grantTypeJwtBearer = "jwt_bearer"
|
||||
)
|
||||
|
||||
var maskedHeaderKeys = map[string]struct{}{
|
||||
"Authorization": {},
|
||||
"X-Amz-Security-Token": {},
|
||||
}
|
||||
|
||||
// An HTTPAuthPlugin represents a mechanism to construct and configure HTTP authentication for a REST service
|
||||
type HTTPAuthPlugin interface {
|
||||
// implementations can assume NewClient will be called before Prepare
|
||||
NewClient(Config) (*http.Client, error)
|
||||
Prepare(*http.Request) error
|
||||
}
|
||||
|
||||
// Config represents configuration for a REST client.
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
AllowInsecureTLS bool `json:"allow_insecure_tls,omitempty"`
|
||||
ResponseHeaderTimeoutSeconds *int64 `json:"response_header_timeout_seconds,omitempty"`
|
||||
TLS *serverTLSConfig `json:"tls,omitempty"`
|
||||
Credentials struct {
|
||||
Bearer *bearerAuthPlugin `json:"bearer,omitempty"`
|
||||
OAuth2 *oauth2ClientCredentialsAuthPlugin `json:"oauth2,omitempty"`
|
||||
ClientTLS *clientTLSAuthPlugin `json:"client_tls,omitempty"`
|
||||
S3Signing *awsSigningAuthPlugin `json:"s3_signing,omitempty"`
|
||||
GCPMetadata *gcpMetadataAuthPlugin `json:"gcp_metadata,omitempty"`
|
||||
AzureManagedIdentity *azureManagedIdentitiesAuthPlugin `json:"azure_managed_identity,omitempty"`
|
||||
Plugin *string `json:"plugin,omitempty"`
|
||||
} `json:"credentials"`
|
||||
Type string `json:"type,omitempty"`
|
||||
keys map[string]*keys.Config
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
// Equal returns true if this client config is equal to the other.
|
||||
func (c *Config) Equal(other *Config) bool {
|
||||
otherWithoutLogger := *other
|
||||
otherWithoutLogger.logger = c.logger
|
||||
return reflect.DeepEqual(c, &otherWithoutLogger)
|
||||
}
|
||||
|
||||
// An AuthPluginLookupFunc can lookup auth plugins by their name.
|
||||
type AuthPluginLookupFunc func(name string) HTTPAuthPlugin
|
||||
|
||||
// AuthPlugin should be used to get an authentication method from the config.
|
||||
func (c *Config) AuthPlugin(lookup AuthPluginLookupFunc) (HTTPAuthPlugin, error) {
|
||||
var candidate HTTPAuthPlugin
|
||||
if c.Credentials.Plugin != nil {
|
||||
if lookup == nil {
|
||||
// if no authPluginLookup function is passed we can't resolve the plugin
|
||||
return nil, errors.New("missing auth plugin lookup function")
|
||||
}
|
||||
|
||||
candidate := lookup(*c.Credentials.Plugin)
|
||||
if candidate == nil {
|
||||
return nil, fmt.Errorf("auth plugin %q not found", *c.Credentials.Plugin)
|
||||
}
|
||||
|
||||
return candidate, nil
|
||||
}
|
||||
// reflection avoids need for this code to change as auth plugins are added
|
||||
s := reflect.ValueOf(c.Credentials)
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
if s.Field(i).IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
if candidate != nil {
|
||||
return nil, errors.New("a maximum one credential method must be specified")
|
||||
}
|
||||
|
||||
candidate = s.Field(i).Interface().(HTTPAuthPlugin)
|
||||
}
|
||||
|
||||
if candidate == nil {
|
||||
return &defaultAuthPlugin{}, nil
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func (c *Config) authHTTPClient(lookup AuthPluginLookupFunc) (*http.Client, error) {
|
||||
plugin, err := c.AuthPlugin(lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plugin.NewClient(*c)
|
||||
}
|
||||
|
||||
func (c *Config) authPrepare(req *http.Request, lookup AuthPluginLookupFunc) error {
|
||||
plugin, err := c.AuthPlugin(lookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plugin.Prepare(req)
|
||||
}
|
||||
|
||||
// Client implements an HTTP/REST client for communicating with remote
|
||||
// services.
|
||||
type Client struct {
|
||||
bytes *[]byte
|
||||
json *interface{}
|
||||
config Config
|
||||
headers map[string]string
|
||||
authPluginLookup AuthPluginLookupFunc
|
||||
logger logging.Logger
|
||||
loggerFields map[string]interface{}
|
||||
distributedTacingOpts tracing.Options
|
||||
}
|
||||
|
||||
// Name returns an option that overrides the service name on the client.
|
||||
func Name(s string) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.config.Name = s
|
||||
}
|
||||
}
|
||||
|
||||
// AuthPluginLookup assigns a function to lookup an HTTPAuthPlugin to a new Client.
|
||||
// It's intended to be used when creating a Client using New(). Usually this is passed
|
||||
// the plugins.AuthPlugin func, which retrieves a registered HTTPAuthPlugin from the
|
||||
// plugin manager.
|
||||
func AuthPluginLookup(l AuthPluginLookupFunc) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.authPluginLookup = l
|
||||
}
|
||||
}
|
||||
|
||||
// Logger assigns a logger to the client
|
||||
func Logger(l logging.Logger) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// DistributedTracingOpts sets the options to be used by distributed tracing.
|
||||
func DistributedTracingOpts(tr tracing.Options) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.distributedTacingOpts = tr
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new Client for config.
|
||||
func New(config []byte, keys map[string]*keys.Config, opts ...func(*Client)) (Client, error) {
|
||||
var parsedConfig Config
|
||||
if err := util.Unmarshal(config, &parsedConfig); err != nil {
|
||||
return Client{}, err
|
||||
}
|
||||
|
||||
parsedConfig.URL = strings.TrimRight(parsedConfig.URL, "/")
|
||||
|
||||
if parsedConfig.ResponseHeaderTimeoutSeconds == nil {
|
||||
timeout := defaultResponseHeaderTimeoutSeconds
|
||||
parsedConfig.ResponseHeaderTimeoutSeconds = &timeout
|
||||
}
|
||||
|
||||
parsedConfig.keys = keys
|
||||
|
||||
client := Client{
|
||||
config: parsedConfig,
|
||||
}
|
||||
|
||||
for _, f := range opts {
|
||||
f(&client)
|
||||
}
|
||||
|
||||
if client.logger == nil {
|
||||
client.logger = logging.Get()
|
||||
}
|
||||
|
||||
client.config.logger = client.logger
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// AuthPluginLookup returns the lookup function to find a custom registered
|
||||
// auth plugin by its name.
|
||||
func (c Client) AuthPluginLookup() AuthPluginLookupFunc {
|
||||
return c.authPluginLookup
|
||||
}
|
||||
|
||||
// Service returns the name of the service this Client is configured for.
|
||||
func (c Client) Service() string {
|
||||
return c.config.Name
|
||||
}
|
||||
|
||||
// Config returns this Client's configuration
|
||||
func (c Client) Config() *Config {
|
||||
return &c.config
|
||||
}
|
||||
|
||||
// SetResponseHeaderTimeout sets the "ResponseHeaderTimeout" in the http client's Transport
|
||||
func (c Client) SetResponseHeaderTimeout(timeout *int64) Client {
|
||||
c.config.ResponseHeaderTimeoutSeconds = timeout
|
||||
return c
|
||||
}
|
||||
|
||||
// Logger returns the logger assigned to the Client
|
||||
func (c Client) Logger() logging.Logger {
|
||||
return c.logger
|
||||
}
|
||||
|
||||
// LoggerFields returns the fields used for log statements used by Client
|
||||
func (c Client) LoggerFields() map[string]interface{} {
|
||||
return c.loggerFields
|
||||
}
|
||||
|
||||
// WithHeader returns a shallow copy of the client with a header to include the
|
||||
// requests.
|
||||
func (c Client) WithHeader(k, v string) Client {
|
||||
if v == "" {
|
||||
return c
|
||||
}
|
||||
if c.headers == nil {
|
||||
c.headers = map[string]string{}
|
||||
}
|
||||
c.headers[k] = v
|
||||
return c
|
||||
}
|
||||
|
||||
// WithJSON returns a shallow copy of the client with the JSON value set as the
|
||||
// message body to include the requests. This function sets the Content-Type
|
||||
// header.
|
||||
func (c Client) WithJSON(body interface{}) Client {
|
||||
c = c.WithHeader("Content-Type", "application/json")
|
||||
c.json = &body
|
||||
return c
|
||||
}
|
||||
|
||||
// WithBytes returns a shallow copy of the client with the bytes set as the
|
||||
// message body to include in the requests.
|
||||
func (c Client) WithBytes(body []byte) Client {
|
||||
c.bytes = &body
|
||||
return c
|
||||
}
|
||||
|
||||
// Do executes a request using the client.
|
||||
func (c Client) Do(ctx context.Context, method, path string) (*http.Response, error) {
|
||||
|
||||
httpClient, err := c.config.authHTTPClient(c.authPluginLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.distributedTacingOpts) > 0 {
|
||||
httpClient.Transport = tracing.NewTransport(httpClient.Transport, c.distributedTacingOpts)
|
||||
}
|
||||
|
||||
path = strings.Trim(path, "/")
|
||||
|
||||
var body io.Reader
|
||||
|
||||
if c.bytes != nil {
|
||||
body = bytes.NewReader(*c.bytes)
|
||||
} else if c.json != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(*c.json); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = &buf
|
||||
}
|
||||
|
||||
url := c.config.URL + "/" + path
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"User-Agent": version.UserAgent,
|
||||
}
|
||||
|
||||
// Copy custom headers from config.
|
||||
for key, value := range c.config.Headers {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
// Overwrite with headers set directly on client.
|
||||
for key, value := range c.headers {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
err = c.config.authPrepare(req, c.authPluginLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.logger.GetLevel() >= logging.Debug {
|
||||
c.loggerFields = map[string]interface{}{
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": withMaskedHeaders(req.Header),
|
||||
}
|
||||
|
||||
c.logger.WithFields(c.loggerFields).Debug("Sending request.")
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
|
||||
if resp != nil && c.logger.GetLevel() >= logging.Debug {
|
||||
// Only log for debug purposes. If an error occurred, the caller should handle
|
||||
// that. In the non-error case, the caller may not do anything.
|
||||
c.loggerFields["status"] = resp.Status
|
||||
c.loggerFields["headers"] = resp.Header
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
dump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(string(dump)) < defaultResponseSizeLimitBytes {
|
||||
c.loggerFields["response"] = string(dump)
|
||||
} else {
|
||||
c.loggerFields["response"] = fmt.Sprintf("%v...", string(dump[:defaultResponseSizeLimitBytes]))
|
||||
}
|
||||
}
|
||||
c.logger.WithFields(c.loggerFields).Debug("Received response.")
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func withMaskedHeaders(headers http.Header) http.Header {
|
||||
masked := make(http.Header)
|
||||
for k, v := range headers {
|
||||
if _, ok := maskedHeaderKeys[k]; ok {
|
||||
masked.Set(k, "REDACTED")
|
||||
} else {
|
||||
masked[k] = v
|
||||
}
|
||||
}
|
||||
return masked
|
||||
}
|
||||
Reference in New Issue
Block a user