|
@@ -39,6 +39,9 @@ type Config struct {
|
|
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
|
// 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.
|
|
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
|
|
ServiceAccountImpersonationURL string
|
|
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
|
|
// 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
|
|
// 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.
|
|
// called with additional basic authentication using client_id as username and client_secret as password.
|
|
@@ -53,6 +56,11 @@ type Config struct {
|
|
QuotaProjectID string
|
|
QuotaProjectID string
|
|
// Scopes contains the desired scopes for the returned access token.
|
|
// Scopes contains the desired scopes for the returned access token.
|
|
Scopes []string
|
|
Scopes []string
|
|
|
|
+ // The optional workforce pool user project number when the credential
|
|
|
|
+ // corresponds to 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.
|
|
|
|
+ WorkforcePoolUserProject string
|
|
}
|
|
}
|
|
|
|
|
|
// Each element consists of a list of patterns. validateURLs checks for matches
|
|
// Each element consists of a list of patterns. validateURLs checks for matches
|
|
@@ -66,13 +74,16 @@ var (
|
|
regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
|
|
regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
|
|
regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
|
|
regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
|
|
regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
|
|
regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
|
|
|
|
+ regexp.MustCompile(`(?i)^sts-[^\.\s\/\\]+\.p\.googleapis\.com$`),
|
|
}
|
|
}
|
|
validImpersonateURLPatterns = []*regexp.Regexp{
|
|
validImpersonateURLPatterns = []*regexp.Regexp{
|
|
regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
|
|
regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
|
|
regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
|
|
regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
|
|
regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
|
|
regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
|
|
regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
|
|
regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
|
|
|
|
+ regexp.MustCompile(`^iamcredentials-[^\.\s\/\\]+\.p\.googleapis\.com$`),
|
|
}
|
|
}
|
|
|
|
+ validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
|
)
|
|
)
|
|
|
|
|
|
func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
|
|
func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
|
|
@@ -86,14 +97,17 @@ func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
|
|
toTest := parsed.Host
|
|
toTest := parsed.Host
|
|
|
|
|
|
for _, pattern := range patterns {
|
|
for _, pattern := range patterns {
|
|
-
|
|
|
|
- if valid := pattern.MatchString(toTest); valid {
|
|
|
|
|
|
+ if pattern.MatchString(toTest) {
|
|
return true
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
return false
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+func validateWorkforceAudience(input string) bool {
|
|
|
|
+ return validWorkforceAudiencePattern.MatchString(input)
|
|
|
|
+}
|
|
|
|
+
|
|
// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
|
|
// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
|
|
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
|
|
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
|
|
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
|
|
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
|
|
@@ -115,6 +129,13 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ if c.WorkforcePoolUserProject != "" {
|
|
|
|
+ valid := validateWorkforceAudience(c.Audience)
|
|
|
|
+ if !valid {
|
|
|
|
+ return nil, fmt.Errorf("oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials")
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
ts := tokenSource{
|
|
ts := tokenSource{
|
|
ctx: ctx,
|
|
ctx: ctx,
|
|
conf: c,
|
|
conf: c,
|
|
@@ -124,11 +145,12 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
|
|
}
|
|
}
|
|
scopes := c.Scopes
|
|
scopes := c.Scopes
|
|
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
|
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
|
- imp := impersonateTokenSource{
|
|
|
|
- ctx: ctx,
|
|
|
|
- url: c.ServiceAccountImpersonationURL,
|
|
|
|
- scopes: scopes,
|
|
|
|
- ts: oauth2.ReuseTokenSource(nil, ts),
|
|
|
|
|
|
+ imp := ImpersonateTokenSource{
|
|
|
|
+ Ctx: ctx,
|
|
|
|
+ URL: c.ServiceAccountImpersonationURL,
|
|
|
|
+ Scopes: scopes,
|
|
|
|
+ Ts: oauth2.ReuseTokenSource(nil, ts),
|
|
|
|
+ TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
|
|
}
|
|
}
|
|
return oauth2.ReuseTokenSource(nil, imp), nil
|
|
return oauth2.ReuseTokenSource(nil, imp), nil
|
|
}
|
|
}
|
|
@@ -147,7 +169,7 @@ type format struct {
|
|
}
|
|
}
|
|
|
|
|
|
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
|
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
|
-// Either the File or the URL field should be filled, depending on the kind of credential in question.
|
|
|
|
|
|
+// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
|
|
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
|
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
|
type CredentialSource struct {
|
|
type CredentialSource struct {
|
|
File string `json:"file"`
|
|
File string `json:"file"`
|
|
@@ -155,33 +177,50 @@ type CredentialSource struct {
|
|
URL string `json:"url"`
|
|
URL string `json:"url"`
|
|
Headers map[string]string `json:"headers"`
|
|
Headers map[string]string `json:"headers"`
|
|
|
|
|
|
|
|
+ Executable *ExecutableConfig `json:"executable"`
|
|
|
|
+
|
|
EnvironmentID string `json:"environment_id"`
|
|
EnvironmentID string `json:"environment_id"`
|
|
RegionURL string `json:"region_url"`
|
|
RegionURL string `json:"region_url"`
|
|
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
|
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
|
CredVerificationURL string `json:"cred_verification_url"`
|
|
CredVerificationURL string `json:"cred_verification_url"`
|
|
|
|
+ IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
|
Format format `json:"format"`
|
|
Format format `json:"format"`
|
|
}
|
|
}
|
|
|
|
|
|
-// parse determines the type of CredentialSource needed
|
|
|
|
|
|
+type ExecutableConfig struct {
|
|
|
|
+ Command string `json:"command"`
|
|
|
|
+ TimeoutMillis *int `json:"timeout_millis"`
|
|
|
|
+ OutputFile string `json:"output_file"`
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// parse determines the type of CredentialSource needed.
|
|
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
|
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
|
if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
|
if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
|
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
|
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
|
if awsVersion != 1 {
|
|
if awsVersion != 1 {
|
|
return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
|
|
return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
|
|
}
|
|
}
|
|
- return awsCredentialSource{
|
|
|
|
|
|
+
|
|
|
|
+ awsCredSource := awsCredentialSource{
|
|
EnvironmentID: c.CredentialSource.EnvironmentID,
|
|
EnvironmentID: c.CredentialSource.EnvironmentID,
|
|
RegionURL: c.CredentialSource.RegionURL,
|
|
RegionURL: c.CredentialSource.RegionURL,
|
|
RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
|
RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
|
CredVerificationURL: c.CredentialSource.URL,
|
|
CredVerificationURL: c.CredentialSource.URL,
|
|
TargetResource: c.Audience,
|
|
TargetResource: c.Audience,
|
|
ctx: ctx,
|
|
ctx: ctx,
|
|
- }, nil
|
|
|
|
|
|
+ }
|
|
|
|
+ if c.CredentialSource.IMDSv2SessionTokenURL != "" {
|
|
|
|
+ awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return awsCredSource, nil
|
|
}
|
|
}
|
|
} else if c.CredentialSource.File != "" {
|
|
} else if c.CredentialSource.File != "" {
|
|
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
|
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
|
} else if c.CredentialSource.URL != "" {
|
|
} else if c.CredentialSource.URL != "" {
|
|
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
|
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
|
|
|
+ } else if c.CredentialSource.Executable != nil {
|
|
|
|
+ return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
|
|
return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
|
|
}
|
|
}
|
|
@@ -224,7 +263,15 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
|
|
ClientID: conf.ClientID,
|
|
ClientID: conf.ClientID,
|
|
ClientSecret: conf.ClientSecret,
|
|
ClientSecret: conf.ClientSecret,
|
|
}
|
|
}
|
|
- stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil)
|
|
|
|
|
|
+ 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 conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
|
|
|
|
+ options = map[string]interface{}{
|
|
|
|
+ "userProject": conf.WorkforcePoolUserProject,
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
|
|
if err != nil {
|
|
if err != nil {
|
|
return nil, err
|
|
return nil, err
|
|
}
|
|
}
|