peridot/vendor/cloud.google.com/go/auth/credentials/internal/externalaccount/externalaccount.go
2024-10-16 12:56:53 +02:00

368 lines
14 KiB
Go

// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package externalaccount
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials/internal/impersonate"
"cloud.google.com/go/auth/credentials/internal/stsexchange"
"cloud.google.com/go/auth/internal/credsfile"
)
const (
timeoutMinimum = 5 * time.Second
timeoutMaximum = 120 * time.Second
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
defaultUniverseDomain = "googleapis.com"
)
var (
// Now aliases time.Now for testing
Now = func() time.Time {
return time.Now().UTC()
}
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
)
// Options stores the configuration for fetching tokens with external credentials.
type Options struct {
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
// identity pool or the workforce pool and the provider identifier in that pool.
Audience string
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
SubjectTokenType string
// TokenURL is the STS token exchange endpoint.
TokenURL string
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
// user attributes like account identifier, eg. email, username, uid, etc). This is
// needed for gCloud session account identification.
TokenInfoURL string
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
ServiceAccountImpersonationURL string
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
// token will be valid for.
ServiceAccountImpersonationLifetimeSeconds int
// ClientSecret is currently only required if token_info endpoint also
// needs to be called with the generated GCP access token. When provided, STS will be
// called with additional basic authentication using client_id as username and client_secret as password.
ClientSecret string
// ClientID is only required in conjunction with ClientSecret, as described above.
ClientID string
// CredentialSource contains the necessary information to retrieve the token itself, as well
// as some environmental information.
CredentialSource *credsfile.CredentialSource
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
// will set the x-goog-user-project which overrides the project associated with the credentials.
QuotaProjectID string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
// WorkforcePoolUserProject should be set when it is a workforce pool and
// not a workload identity pool. The underlying principal must still have
// serviceusage.services.use IAM permission to use the project for
// billing/quota. Optional.
WorkforcePoolUserProject string
// UniverseDomain is the default service domain for a given Cloud universe.
// This value will be used in the default STS token URL. The default value
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
UniverseDomain string
// SubjectTokenProvider is an optional token provider for OIDC/SAML
// credentials. One of SubjectTokenProvider, AWSSecurityCredentialProvider
// or CredentialSource must be provided. Optional.
SubjectTokenProvider SubjectTokenProvider
// AwsSecurityCredentialsProvider is an AWS Security Credential provider
// for AWS credentials. One of SubjectTokenProvider,
// AWSSecurityCredentialProvider or CredentialSource must be provided. Optional.
AwsSecurityCredentialsProvider AwsSecurityCredentialsProvider
// Client for token request.
Client *http.Client
}
// SubjectTokenProvider can be used to supply a subject token to exchange for a
// GCP access token.
type SubjectTokenProvider interface {
// SubjectToken should return a valid subject token or an error.
// The external account token provider does not cache the returned subject
// token, so caching logic should be implemented in the provider to prevent
// multiple requests for the same subject token.
SubjectToken(ctx context.Context, opts *RequestOptions) (string, error)
}
// RequestOptions contains information about the requested subject token or AWS
// security credentials from the Google external account credential.
type RequestOptions struct {
// Audience is the requested audience for the external account credential.
Audience string
// Subject token type is the requested subject token type for the external
// account credential. Expected values include:
// “urn:ietf:params:oauth:token-type:jwt”
// “urn:ietf:params:oauth:token-type:id-token”
// “urn:ietf:params:oauth:token-type:saml2”
// “urn:ietf:params:aws:token-type:aws4_request”
SubjectTokenType string
}
// AwsSecurityCredentialsProvider can be used to supply AwsSecurityCredentials
// and an AWS Region to exchange for a GCP access token.
type AwsSecurityCredentialsProvider interface {
// AwsRegion should return the AWS region or an error.
AwsRegion(ctx context.Context, opts *RequestOptions) (string, error)
// GetAwsSecurityCredentials should return a valid set of
// AwsSecurityCredentials or an error. The external account token provider
// does not cache the returned security credentials, so caching logic should
// be implemented in the provider to prevent multiple requests for the
// same security credentials.
AwsSecurityCredentials(ctx context.Context, opts *RequestOptions) (*AwsSecurityCredentials, error)
}
// AwsSecurityCredentials models AWS security credentials.
type AwsSecurityCredentials struct {
// AccessKeyId is the AWS Access Key ID - Required.
AccessKeyID string `json:"AccessKeyID"`
// SecretAccessKey is the AWS Secret Access Key - Required.
SecretAccessKey string `json:"SecretAccessKey"`
// SessionToken is the AWS Session token. This should be provided for
// temporary AWS security credentials - Optional.
SessionToken string `json:"Token"`
}
func (o *Options) validate() error {
if o.Audience == "" {
return fmt.Errorf("externalaccount: Audience must be set")
}
if o.SubjectTokenType == "" {
return fmt.Errorf("externalaccount: Subject token type must be set")
}
if o.WorkforcePoolUserProject != "" {
if valid := validWorkforceAudiencePattern.MatchString(o.Audience); !valid {
return fmt.Errorf("externalaccount: workforce_pool_user_project should not be set for non-workforce pool credentials")
}
}
count := 0
if o.CredentialSource != nil {
count++
}
if o.SubjectTokenProvider != nil {
count++
}
if o.AwsSecurityCredentialsProvider != nil {
count++
}
if count == 0 {
return fmt.Errorf("externalaccount: one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
}
if count > 1 {
return fmt.Errorf("externalaccount: only one of CredentialSource, SubjectTokenProvider, or AwsSecurityCredentialsProvider must be set")
}
return nil
}
// resolveTokenURL sets the default STS token endpoint with the configured
// universe domain.
func (o *Options) resolveTokenURL() {
if o.TokenURL != "" {
return
} else if o.UniverseDomain != "" {
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, o.UniverseDomain, 1)
} else {
o.TokenURL = strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
}
}
// NewTokenProvider returns a [cloud.google.com/go/auth.TokenProvider]
// configured with the provided options.
func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
if err := opts.validate(); err != nil {
return nil, err
}
opts.resolveTokenURL()
stp, err := newSubjectTokenProvider(opts)
if err != nil {
return nil, err
}
tp := &tokenProvider{
client: opts.Client,
opts: opts,
stp: stp,
}
if opts.ServiceAccountImpersonationURL == "" {
return auth.NewCachedTokenProvider(tp, nil), nil
}
scopes := make([]string, len(opts.Scopes))
copy(scopes, opts.Scopes)
// needed for impersonation
tp.opts.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
imp, err := impersonate.NewTokenProvider(&impersonate.Options{
Client: opts.Client,
URL: opts.ServiceAccountImpersonationURL,
Scopes: scopes,
Tp: auth.NewCachedTokenProvider(tp, nil),
TokenLifetimeSeconds: opts.ServiceAccountImpersonationLifetimeSeconds,
})
if err != nil {
return nil, err
}
return auth.NewCachedTokenProvider(imp, nil), nil
}
type subjectTokenProvider interface {
subjectToken(ctx context.Context) (string, error)
providerType() string
}
// tokenProvider is the provider that handles external credentials. It is used to retrieve Tokens.
type tokenProvider struct {
client *http.Client
opts *Options
stp subjectTokenProvider
}
func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
subjectToken, err := tp.stp.subjectToken(ctx)
if err != nil {
return nil, err
}
stsRequest := &stsexchange.TokenRequest{
GrantType: stsexchange.GrantType,
Audience: tp.opts.Audience,
Scope: tp.opts.Scopes,
RequestedTokenType: stsexchange.TokenType,
SubjectToken: subjectToken,
SubjectTokenType: tp.opts.SubjectTokenType,
}
header := make(http.Header)
header.Set("Content-Type", "application/x-www-form-urlencoded")
header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
clientAuth := stsexchange.ClientAuthentication{
AuthStyle: auth.StyleInHeader,
ClientID: tp.opts.ClientID,
ClientSecret: tp.opts.ClientSecret,
}
var options map[string]interface{}
// Do not pass workforce_pool_user_project when client authentication is used.
// The client ID is sufficient for determining the user project.
if tp.opts.WorkforcePoolUserProject != "" && tp.opts.ClientID == "" {
options = map[string]interface{}{
"userProject": tp.opts.WorkforcePoolUserProject,
}
}
stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
Client: tp.client,
Endpoint: tp.opts.TokenURL,
Request: stsRequest,
Authentication: clientAuth,
Headers: header,
ExtraOpts: options,
})
if err != nil {
return nil, err
}
tok := &auth.Token{
Value: stsResp.AccessToken,
Type: stsResp.TokenType,
}
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
if stsResp.ExpiresIn <= 0 {
return nil, fmt.Errorf("credentials: got invalid expiry from security token service")
}
tok.Expiry = Now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
return tok, nil
}
// newSubjectTokenProvider determines the type of credsfile.CredentialSource needed to create a
// subjectTokenProvider
func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
reqOpts := &RequestOptions{Audience: o.Audience, SubjectTokenType: o.SubjectTokenType}
if o.AwsSecurityCredentialsProvider != nil {
return &awsSubjectProvider{
securityCredentialsProvider: o.AwsSecurityCredentialsProvider,
TargetResource: o.Audience,
reqOpts: reqOpts,
}, nil
} else if o.SubjectTokenProvider != nil {
return &programmaticProvider{stp: o.SubjectTokenProvider, opts: reqOpts}, nil
} else if len(o.CredentialSource.EnvironmentID) > 3 && o.CredentialSource.EnvironmentID[:3] == "aws" {
if awsVersion, err := strconv.Atoi(o.CredentialSource.EnvironmentID[3:]); err == nil {
if awsVersion != 1 {
return nil, fmt.Errorf("credentials: aws version '%d' is not supported in the current build", awsVersion)
}
awsProvider := &awsSubjectProvider{
EnvironmentID: o.CredentialSource.EnvironmentID,
RegionURL: o.CredentialSource.RegionURL,
RegionalCredVerificationURL: o.CredentialSource.RegionalCredVerificationURL,
CredVerificationURL: o.CredentialSource.URL,
TargetResource: o.Audience,
Client: o.Client,
}
if o.CredentialSource.IMDSv2SessionTokenURL != "" {
awsProvider.IMDSv2SessionTokenURL = o.CredentialSource.IMDSv2SessionTokenURL
}
return awsProvider, nil
}
} else if o.CredentialSource.File != "" {
return &fileSubjectProvider{File: o.CredentialSource.File, Format: o.CredentialSource.Format}, nil
} else if o.CredentialSource.URL != "" {
return &urlSubjectProvider{URL: o.CredentialSource.URL, Headers: o.CredentialSource.Headers, Format: o.CredentialSource.Format, Client: o.Client}, nil
} else if o.CredentialSource.Executable != nil {
ec := o.CredentialSource.Executable
if ec.Command == "" {
return nil, errors.New("credentials: missing `command` field — executable command must be provided")
}
execProvider := &executableSubjectProvider{}
execProvider.Command = ec.Command
if ec.TimeoutMillis == 0 {
execProvider.Timeout = executableDefaultTimeout
} else {
execProvider.Timeout = time.Duration(ec.TimeoutMillis) * time.Millisecond
if execProvider.Timeout < timeoutMinimum || execProvider.Timeout > timeoutMaximum {
return nil, fmt.Errorf("credentials: invalid `timeout_millis` field — executable timeout must be between %v and %v seconds", timeoutMinimum.Seconds(), timeoutMaximum.Seconds())
}
}
execProvider.OutputFile = ec.OutputFile
execProvider.client = o.Client
execProvider.opts = o
execProvider.env = runtimeEnvironment{}
return execProvider, nil
}
return nil, errors.New("credentials: unable to parse credential source")
}
func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
goVersion(),
"unknown",
p.providerType(),
conf.ServiceAccountImpersonationURL != "",
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
}