chore: migrate to gitea
Some checks failed
golangci-lint / lint (push) Failing after 21s
Test / test (push) Failing after 2m17s

This commit is contained in:
2026-01-27 01:40:31 +01:00
parent a9bca767a9
commit 1a27ed5274
3163 changed files with 1216358 additions and 1529 deletions

View File

@@ -0,0 +1,63 @@
// 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 credsfile is meant to hide implementation details from the pubic
// surface of the detect package. It should not import any other packages in
// this module. It is located under the main internal package so other
// sub-packages can use these parsed types as well.
package credsfile
import (
"os"
"os/user"
"path/filepath"
"runtime"
)
const (
// GoogleAppCredsEnvVar is the environment variable for setting the
// application default credentials.
GoogleAppCredsEnvVar = "GOOGLE_APPLICATION_CREDENTIALS"
userCredsFilename = "application_default_credentials.json"
)
// GetFileNameFromEnv returns the override if provided or detects a filename
// from the environment.
func GetFileNameFromEnv(override string) string {
if override != "" {
return override
}
return os.Getenv(GoogleAppCredsEnvVar)
}
// GetWellKnownFileName tries to locate the filepath for the user credential
// file based on the environment.
func GetWellKnownFileName() string {
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("APPDATA"), "gcloud", userCredsFilename)
}
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", userCredsFilename)
}
// guessUnixHomeDir default to checking for HOME, but not all unix systems have
// this set, do have a fallback.
func guessUnixHomeDir() string {
if v := os.Getenv("HOME"); v != "" {
return v
}
if u, err := user.Current(); err == nil {
return u.HomeDir
}
return ""
}

View File

@@ -0,0 +1,159 @@
// 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 credsfile
import (
"encoding/json"
)
// Config3LO is the internals of a client creds file.
type Config3LO struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURIs []string `json:"redirect_uris"`
AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
}
// ClientCredentialsFile representation.
type ClientCredentialsFile struct {
Web *Config3LO `json:"web"`
Installed *Config3LO `json:"installed"`
UniverseDomain string `json:"universe_domain"`
}
// ServiceAccountFile representation.
type ServiceAccountFile struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
AuthURL string `json:"auth_uri"`
TokenURL string `json:"token_uri"`
UniverseDomain string `json:"universe_domain"`
}
// UserCredentialsFile representation.
type UserCredentialsFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
QuotaProjectID string `json:"quota_project_id"`
RefreshToken string `json:"refresh_token"`
UniverseDomain string `json:"universe_domain"`
}
// ExternalAccountFile representation.
type ExternalAccountFile struct {
Type string `json:"type"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Audience string `json:"audience"`
SubjectTokenType string `json:"subject_token_type"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
TokenURL string `json:"token_url"`
CredentialSource *CredentialSource `json:"credential_source,omitempty"`
TokenInfoURL string `json:"token_info_url"`
ServiceAccountImpersonation *ServiceAccountImpersonationInfo `json:"service_account_impersonation,omitempty"`
QuotaProjectID string `json:"quota_project_id"`
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
UniverseDomain string `json:"universe_domain"`
}
// ExternalAccountAuthorizedUserFile representation.
type ExternalAccountAuthorizedUserFile struct {
Type string `json:"type"`
Audience string `json:"audience"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RefreshToken string `json:"refresh_token"`
TokenURL string `json:"token_url"`
TokenInfoURL string `json:"token_info_url"`
RevokeURL string `json:"revoke_url"`
QuotaProjectID string `json:"quota_project_id"`
UniverseDomain string `json:"universe_domain"`
}
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
//
// One field amongst File, URL, Certificate, 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.
type CredentialSource struct {
File string `json:"file"`
URL string `json:"url"`
Headers map[string]string `json:"headers"`
Executable *ExecutableConfig `json:"executable,omitempty"`
Certificate *CertificateConfig `json:"certificate"`
EnvironmentID string `json:"environment_id"` // TODO: Make type for this
RegionURL string `json:"region_url"`
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
CredVerificationURL string `json:"cred_verification_url"`
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
Format *Format `json:"format,omitempty"`
}
// Format describes the format of a [CredentialSource].
type Format struct {
// Type is either "text" or "json". When not provided "text" type is assumed.
Type string `json:"type"`
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
SubjectTokenFieldName string `json:"subject_token_field_name"`
}
// ExecutableConfig represents the command to run for an executable
// [CredentialSource].
type ExecutableConfig struct {
Command string `json:"command"`
TimeoutMillis int `json:"timeout_millis"`
OutputFile string `json:"output_file"`
}
// CertificateConfig represents the options used to set up X509 based workload
// [CredentialSource]
type CertificateConfig struct {
UseDefaultCertificateConfig bool `json:"use_default_certificate_config"`
CertificateConfigLocation string `json:"certificate_config_location"`
TrustChainPath string `json:"trust_chain_path"`
}
// ServiceAccountImpersonationInfo has impersonation configuration.
type ServiceAccountImpersonationInfo struct {
TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
}
// ImpersonatedServiceAccountFile representation.
type ImpersonatedServiceAccountFile struct {
Type string `json:"type"`
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
Delegates []string `json:"delegates"`
Scopes []string `json:"scopes"`
CredSource json.RawMessage `json:"source_credentials"`
UniverseDomain string `json:"universe_domain"`
}
// GDCHServiceAccountFile represents the Google Distributed Cloud Hosted (GDCH) service identity file.
type GDCHServiceAccountFile struct {
Type string `json:"type"`
FormatVersion string `json:"format_version"`
Project string `json:"project"`
Name string `json:"name"`
CertPath string `json:"ca_cert_path"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
TokenURL string `json:"token_uri"`
UniverseDomain string `json:"universe_domain"`
}

View File

@@ -0,0 +1,99 @@
// 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 credsfile
import (
"encoding/json"
)
// ParseServiceAccount parses bytes into a [ServiceAccountFile].
func ParseServiceAccount(b []byte) (*ServiceAccountFile, error) {
var f *ServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseClientCredentials parses bytes into a
// [credsfile.ClientCredentialsFile].
func ParseClientCredentials(b []byte) (*ClientCredentialsFile, error) {
var f *ClientCredentialsFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseUserCredentials parses bytes into a [UserCredentialsFile].
func ParseUserCredentials(b []byte) (*UserCredentialsFile, error) {
var f *UserCredentialsFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseExternalAccount parses bytes into a [ExternalAccountFile].
func ParseExternalAccount(b []byte) (*ExternalAccountFile, error) {
var f *ExternalAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseExternalAccountAuthorizedUser parses bytes into a
// [ExternalAccountAuthorizedUserFile].
func ParseExternalAccountAuthorizedUser(b []byte) (*ExternalAccountAuthorizedUserFile, error) {
var f *ExternalAccountAuthorizedUserFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseImpersonatedServiceAccount parses bytes into a
// [ImpersonatedServiceAccountFile].
func ParseImpersonatedServiceAccount(b []byte) (*ImpersonatedServiceAccountFile, error) {
var f *ImpersonatedServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
// ParseGDCHServiceAccount parses bytes into a [GDCHServiceAccountFile].
func ParseGDCHServiceAccount(b []byte) (*GDCHServiceAccountFile, error) {
var f *GDCHServiceAccountFile
if err := json.Unmarshal(b, &f); err != nil {
return nil, err
}
return f, nil
}
type fileTypeChecker struct {
Type string `json:"type"`
}
// ParseFileType determines the [CredentialType] based on bytes provided.
// Only returns error for json.Unmarshal.
func ParseFileType(b []byte) (string, error) {
var f fileTypeChecker
if err := json.Unmarshal(b, &f); err != nil {
return "", err
}
return f.Type, nil
}

285
vendor/cloud.google.com/go/auth/internal/internal.go generated vendored Normal file
View File

@@ -0,0 +1,285 @@
// 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 internal
import (
"context"
"crypto"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"
"sync"
"time"
"cloud.google.com/go/compute/metadata"
)
const (
// TokenTypeBearer is the auth header prefix for bearer tokens.
TokenTypeBearer = "Bearer"
// QuotaProjectEnvVar is the environment variable for setting the quota
// project.
QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
// UniverseDomainEnvVar is the environment variable for setting the default
// service domain for a given Cloud universe.
UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
projectEnvVar = "GOOGLE_CLOUD_PROJECT"
maxBodySize = 1 << 20
// DefaultUniverseDomain is the default value for universe domain.
// Universe domain is the default service domain for a given Cloud universe.
DefaultUniverseDomain = "googleapis.com"
// TrustBoundaryNoOp is a constant indicating no trust boundary is enforced.
TrustBoundaryNoOp = "0x0"
// TrustBoundaryDataKey is the key used to store trust boundary data in a token's metadata.
TrustBoundaryDataKey = "google.auth.trust_boundary_data"
)
type clonableTransport interface {
Clone() *http.Transport
}
// DefaultClient returns an [http.Client] with some defaults set. If
// the current [http.DefaultTransport] is a [clonableTransport], as
// is the case for an [*http.Transport], the clone will be used.
// Otherwise the [http.DefaultTransport] is used directly.
func DefaultClient() *http.Client {
if transport, ok := http.DefaultTransport.(clonableTransport); ok {
return &http.Client{
Transport: transport.Clone(),
Timeout: 30 * time.Second,
}
}
return &http.Client{
Transport: http.DefaultTransport,
Timeout: 30 * time.Second,
}
}
// ParseKey converts the binary contents of a private key file
// to an crypto.Signer. It detects whether the private key is in a
// PEM container or not. If so, it extracts the the private key
// from PEM container before conversion. It only supports PEM
// containers with no passphrase.
func ParseKey(key []byte) (crypto.Signer, error) {
block, _ := pem.Decode(key)
if block != nil {
key = block.Bytes
}
var parsedKey crypto.PrivateKey
var errPKCS8, errPKCS1, errEC error
if parsedKey, errPKCS8 = x509.ParsePKCS8PrivateKey(key); errPKCS8 != nil {
if parsedKey, errPKCS1 = x509.ParsePKCS1PrivateKey(key); errPKCS1 != nil {
if parsedKey, errEC = x509.ParseECPrivateKey(key); errEC != nil {
return nil, fmt.Errorf("failed to parse private key. Tried PKCS8, PKCS1, and EC formats. Errors: [PKCS8: %v], [PKCS1: %v], [EC: %v]", errPKCS8, errPKCS1, errEC)
}
}
}
parsed, ok := parsedKey.(crypto.Signer)
if !ok {
return nil, errors.New("private key is not a signer")
}
return parsed, nil
}
// GetQuotaProject retrieves quota project with precedence being: override,
// environment variable, creds json file.
func GetQuotaProject(b []byte, override string) string {
if override != "" {
return override
}
if env := os.Getenv(QuotaProjectEnvVar); env != "" {
return env
}
if b == nil {
return ""
}
var v struct {
QuotaProject string `json:"quota_project_id"`
}
if err := json.Unmarshal(b, &v); err != nil {
return ""
}
return v.QuotaProject
}
// GetProjectID retrieves project with precedence being: override,
// environment variable, creds json file.
func GetProjectID(b []byte, override string) string {
if override != "" {
return override
}
if env := os.Getenv(projectEnvVar); env != "" {
return env
}
if b == nil {
return ""
}
var v struct {
ProjectID string `json:"project_id"` // standard service account key
Project string `json:"project"` // gdch key
}
if err := json.Unmarshal(b, &v); err != nil {
return ""
}
if v.ProjectID != "" {
return v.ProjectID
}
return v.Project
}
// DoRequest executes the provided req with the client. It reads the response
// body, closes it, and returns it.
func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
resp, err := client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
if err != nil {
return nil, nil, err
}
return resp, body, nil
}
// ReadAll consumes the whole reader and safely reads the content of its body
// with some overflow protection.
func ReadAll(r io.Reader) ([]byte, error) {
return io.ReadAll(io.LimitReader(r, maxBodySize))
}
// StaticCredentialsProperty is a helper for creating static credentials
// properties.
func StaticCredentialsProperty(s string) StaticProperty {
return StaticProperty(s)
}
// StaticProperty always returns that value of the underlying string.
type StaticProperty string
// GetProperty loads the properly value provided the given context.
func (p StaticProperty) GetProperty(context.Context) (string, error) {
return string(p), nil
}
// ComputeUniverseDomainProvider fetches the credentials universe domain from
// the google cloud metadata service.
type ComputeUniverseDomainProvider struct {
MetadataClient *metadata.Client
universeDomainOnce sync.Once
universeDomain string
universeDomainErr error
}
// GetProperty fetches the credentials universe domain from the google cloud
// metadata service.
func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
c.universeDomainOnce.Do(func() {
c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
})
if c.universeDomainErr != nil {
return "", c.universeDomainErr
}
return c.universeDomain, nil
}
// httpGetMetadataUniverseDomain is a package var for unit test substitution.
var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
return client.GetWithContext(ctx, "universe/universe-domain")
}
func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
if err == nil {
return universeDomain, nil
}
if _, ok := err.(metadata.NotDefinedError); ok {
// http.StatusNotFound (404)
return DefaultUniverseDomain, nil
}
return "", err
}
// FormatIAMServiceAccountResource sets a service account name in an IAM resource
// name.
func FormatIAMServiceAccountResource(name string) string {
return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
}
// TrustBoundaryData represents the trust boundary data associated with a token.
// It contains information about the regions or environments where the token is valid.
type TrustBoundaryData struct {
// Locations is the list of locations that the token is allowed to be used in.
Locations []string
// EncodedLocations represents the locations in an encoded format.
EncodedLocations string
}
// NewTrustBoundaryData returns a new TrustBoundaryData with the specified locations and encoded locations.
func NewTrustBoundaryData(locations []string, encodedLocations string) *TrustBoundaryData {
// Ensure consistency by treating a nil slice as an empty slice.
if locations == nil {
locations = []string{}
}
locationsCopy := make([]string, len(locations))
copy(locationsCopy, locations)
return &TrustBoundaryData{
Locations: locationsCopy,
EncodedLocations: encodedLocations,
}
}
// NewNoOpTrustBoundaryData returns a new TrustBoundaryData with no restrictions.
func NewNoOpTrustBoundaryData() *TrustBoundaryData {
return &TrustBoundaryData{
Locations: []string{},
EncodedLocations: TrustBoundaryNoOp,
}
}
// TrustBoundaryHeader returns the value for the x-allowed-locations header and a bool
// indicating if the header should be set. The return values are structured to
// handle three distinct states required by the backend:
// 1. Header not set: (value="", present=false) -> data is empty.
// 2. Header set to an empty string: (value="", present=true) -> data is a no-op.
// 3. Header set to a value: (value="...", present=true) -> data has locations.
func (t TrustBoundaryData) TrustBoundaryHeader() (value string, present bool) {
if t.EncodedLocations == "" {
// If the data is empty, the header should not be present.
return "", false
}
// If data is not empty, the header should always be present.
present = true
value = ""
if t.EncodedLocations != TrustBoundaryNoOp {
value = t.EncodedLocations
}
// For a no-op, the backend requires an empty string.
return value, present
}

171
vendor/cloud.google.com/go/auth/internal/jwt/jwt.go generated vendored Normal file
View File

@@ -0,0 +1,171 @@
// 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 jwt
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
)
const (
// HeaderAlgRSA256 is the RS256 [Header.Algorithm].
HeaderAlgRSA256 = "RS256"
// HeaderAlgES256 is the ES256 [Header.Algorithm].
HeaderAlgES256 = "ES256"
// HeaderType is the standard [Header.Type].
HeaderType = "JWT"
)
// Header represents a JWT header.
type Header struct {
Algorithm string `json:"alg"`
Type string `json:"typ"`
KeyID string `json:"kid"`
}
func (h *Header) encode() (string, error) {
b, err := json.Marshal(h)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Claims represents the claims set of a JWT.
type Claims struct {
// Iss is the issuer JWT claim.
Iss string `json:"iss"`
// Scope is the scope JWT claim.
Scope string `json:"scope,omitempty"`
// Exp is the expiry JWT claim. If unset, default is in one hour from now.
Exp int64 `json:"exp"`
// Iat is the subject issued at claim. If unset, default is now.
Iat int64 `json:"iat"`
// Aud is the audience JWT claim. Optional.
Aud string `json:"aud"`
// Sub is the subject JWT claim. Optional.
Sub string `json:"sub,omitempty"`
// AdditionalClaims contains any additional non-standard JWT claims. Optional.
AdditionalClaims map[string]interface{} `json:"-"`
}
func (c *Claims) encode() (string, error) {
// Compensate for skew
now := time.Now().Add(-10 * time.Second)
if c.Iat == 0 {
c.Iat = now.Unix()
}
if c.Exp == 0 {
c.Exp = now.Add(time.Hour).Unix()
}
if c.Exp < c.Iat {
return "", fmt.Errorf("jwt: invalid Exp = %d; must be later than Iat = %d", c.Exp, c.Iat)
}
b, err := json.Marshal(c)
if err != nil {
return "", err
}
if len(c.AdditionalClaims) == 0 {
return base64.RawURLEncoding.EncodeToString(b), nil
}
// Marshal private claim set and then append it to b.
prv, err := json.Marshal(c.AdditionalClaims)
if err != nil {
return "", fmt.Errorf("invalid map of additional claims %v: %w", c.AdditionalClaims, err)
}
// Concatenate public and private claim JSON objects.
if !bytes.HasSuffix(b, []byte{'}'}) {
return "", fmt.Errorf("invalid JSON %s", b)
}
if !bytes.HasPrefix(prv, []byte{'{'}) {
return "", fmt.Errorf("invalid JSON %s", prv)
}
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
b = append(b, prv[1:]...) // Append private claims.
return base64.RawURLEncoding.EncodeToString(b), nil
}
// EncodeJWS encodes the data using the provided key as a JSON web signature.
func EncodeJWS(header *Header, c *Claims, signer crypto.Signer) (string, error) {
head, err := header.encode()
if err != nil {
return "", err
}
claims, err := c.encode()
if err != nil {
return "", err
}
ss := fmt.Sprintf("%s.%s", head, claims)
h := sha256.New()
h.Write([]byte(ss))
sig, err := signer.Sign(rand.Reader, h.Sum(nil), crypto.SHA256)
if err != nil {
return "", err
}
return fmt.Sprintf("%s.%s", ss, base64.RawURLEncoding.EncodeToString(sig)), nil
}
// DecodeJWS decodes a claim set from a JWS payload.
func DecodeJWS(payload string) (*Claims, error) {
// decode returned id token to get expiry
s := strings.Split(payload, ".")
if len(s) < 2 {
return nil, errors.New("invalid token received")
}
decoded, err := base64.RawURLEncoding.DecodeString(s[1])
if err != nil {
return nil, err
}
c := &Claims{}
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(c); err != nil {
return nil, err
}
if err := json.NewDecoder(bytes.NewBuffer(decoded)).Decode(&c.AdditionalClaims); err != nil {
return nil, err
}
return c, err
}
// VerifyJWS tests whether the provided JWT token's signature was produced by
// the private key associated with the provided public key.
func VerifyJWS(token string, key *rsa.PublicKey) error {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return errors.New("jwt: invalid token received, token must have 3 parts")
}
signedContent := parts[0] + "." + parts[1]
signatureString, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return err
}
h := sha256.New()
h.Write([]byte(signedContent))
return rsa.VerifyPKCS1v15(key, crypto.SHA256, h.Sum(nil), signatureString)
}

117
vendor/cloud.google.com/go/auth/internal/retry/retry.go generated vendored Normal file
View File

@@ -0,0 +1,117 @@
// Copyright 2025 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 retry
import (
"context"
"io"
"math/rand"
"net/http"
"time"
)
const (
maxRetryAttempts = 5
)
var (
syscallRetryable = func(error) bool { return false }
)
// defaultBackoff is basically equivalent to gax.Backoff without the need for
// the dependency.
type defaultBackoff struct {
max time.Duration
mul float64
cur time.Duration
}
func (b *defaultBackoff) Pause() time.Duration {
d := time.Duration(1 + rand.Int63n(int64(b.cur)))
b.cur = time.Duration(float64(b.cur) * b.mul)
if b.cur > b.max {
b.cur = b.max
}
return d
}
// Sleep is the equivalent of gax.Sleep without the need for the dependency.
func Sleep(ctx context.Context, d time.Duration) error {
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
return ctx.Err()
case <-t.C:
return nil
}
}
// New returns a new Retryer with the default backoff strategy.
func New() *Retryer {
return &Retryer{bo: &defaultBackoff{
cur: 100 * time.Millisecond,
max: 30 * time.Second,
mul: 2,
}}
}
type backoff interface {
Pause() time.Duration
}
// Retryer is a retryer for HTTP requests.
type Retryer struct {
bo backoff
attempts int
}
// Retry determines if a request should be retried.
func (r *Retryer) Retry(status int, err error) (time.Duration, bool) {
if status == http.StatusOK {
return 0, false
}
retryOk := shouldRetry(status, err)
if !retryOk {
return 0, false
}
if r.attempts == maxRetryAttempts {
return 0, false
}
r.attempts++
return r.bo.Pause(), true
}
func shouldRetry(status int, err error) bool {
if 500 <= status && status <= 599 {
return true
}
if err == io.ErrUnexpectedEOF {
return true
}
// Transient network errors should be retried.
if syscallRetryable(err) {
return true
}
if err, ok := err.(interface{ Temporary() bool }); ok {
if err.Temporary() {
return true
}
}
if err, ok := err.(interface{ Unwrap() error }); ok {
return shouldRetry(status, err.Unwrap())
}
return false
}

View File

@@ -0,0 +1,361 @@
// 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 transport
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"log"
"log/slog"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/transport/cert"
"github.com/google/s2a-go"
"google.golang.org/grpc/credentials"
)
const (
mTLSModeAlways = "always"
mTLSModeNever = "never"
mTLSModeAuto = "auto"
// Experimental: if true, the code will try MTLS with S2A as the default for transport security. Default value is false.
googleAPIUseS2AEnv = "EXPERIMENTAL_GOOGLE_API_USE_S2A"
googleAPIUseCertSource = "GOOGLE_API_USE_CLIENT_CERTIFICATE"
googleAPIUseMTLS = "GOOGLE_API_USE_MTLS_ENDPOINT"
googleAPIUseMTLSOld = "GOOGLE_API_USE_MTLS"
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
mtlsMDSRoot = "/run/google-mds-mtls/root.crt"
mtlsMDSKey = "/run/google-mds-mtls/client.key"
)
// Type represents the type of transport used.
type Type int
const (
// TransportTypeUnknown represents an unknown transport type and is the default option.
TransportTypeUnknown Type = iota
// TransportTypeMTLSS2A represents the mTLS transport type using S2A.
TransportTypeMTLSS2A
)
// Options is a struct that is duplicated information from the individual
// transport packages in order to avoid cyclic deps. It correlates 1:1 with
// fields on httptransport.Options and grpctransport.Options.
type Options struct {
Endpoint string
DefaultEndpointTemplate string
DefaultMTLSEndpoint string
ClientCertProvider cert.Provider
Client *http.Client
UniverseDomain string
EnableDirectPath bool
EnableDirectPathXds bool
Logger *slog.Logger
}
// getUniverseDomain returns the default service domain for a given Cloud
// universe.
func (o *Options) getUniverseDomain() string {
if o.UniverseDomain == "" {
return internal.DefaultUniverseDomain
}
return o.UniverseDomain
}
// isUniverseDomainGDU returns true if the universe domain is the default Google
// universe.
func (o *Options) isUniverseDomainGDU() bool {
return o.getUniverseDomain() == internal.DefaultUniverseDomain
}
// defaultEndpoint returns the DefaultEndpointTemplate merged with the
// universe domain if the DefaultEndpointTemplate is set, otherwise returns an
// empty string.
func (o *Options) defaultEndpoint() string {
if o.DefaultEndpointTemplate == "" {
return ""
}
return strings.Replace(o.DefaultEndpointTemplate, universeDomainPlaceholder, o.getUniverseDomain(), 1)
}
// defaultMTLSEndpoint returns the DefaultMTLSEndpointTemplate merged with the
// universe domain if the DefaultMTLSEndpointTemplate is set, otherwise returns an
// empty string.
func (o *Options) defaultMTLSEndpoint() string {
if o.DefaultMTLSEndpoint == "" {
return ""
}
return strings.Replace(o.DefaultMTLSEndpoint, universeDomainPlaceholder, o.getUniverseDomain(), 1)
}
// mergedEndpoint merges a user-provided Endpoint of format host[:port] with the
// default endpoint.
func (o *Options) mergedEndpoint() (string, error) {
defaultEndpoint := o.defaultEndpoint()
u, err := url.Parse(fixScheme(defaultEndpoint))
if err != nil {
return "", err
}
return strings.Replace(defaultEndpoint, u.Host, o.Endpoint, 1), nil
}
func fixScheme(baseURL string) string {
if !strings.Contains(baseURL, "://") {
baseURL = "https://" + baseURL
}
return baseURL
}
// GRPCTransportCredentials embeds interface TransportCredentials with additional data.
type GRPCTransportCredentials struct {
credentials.TransportCredentials
Endpoint string
TransportType Type
}
// GetGRPCTransportCredsAndEndpoint returns an instance of
// [google.golang.org/grpc/credentials.TransportCredentials], and the
// corresponding endpoint and transport type to use for GRPC client.
func GetGRPCTransportCredsAndEndpoint(opts *Options) (*GRPCTransportCredentials, error) {
config, err := getTransportConfig(opts)
if err != nil {
return nil, err
}
defaultTransportCreds := credentials.NewTLS(&tls.Config{
GetClientCertificate: config.clientCertSource,
})
var s2aAddr string
var transportCredsForS2A credentials.TransportCredentials
if config.mtlsS2AAddress != "" {
s2aAddr = config.mtlsS2AAddress
transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
if err != nil {
log.Printf("Loading MTLS MDS credentials failed: %v", err)
if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return &GRPCTransportCredentials{defaultTransportCreds, config.endpoint, TransportTypeUnknown}, nil
}
}
} else if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return &GRPCTransportCredentials{defaultTransportCreds, config.endpoint, TransportTypeUnknown}, nil
}
s2aTransportCreds, err := s2a.NewClientCreds(&s2a.ClientOptions{
S2AAddress: s2aAddr,
TransportCreds: transportCredsForS2A,
})
if err != nil {
// Use default if we cannot initialize S2A client transport credentials.
return &GRPCTransportCredentials{defaultTransportCreds, config.endpoint, TransportTypeUnknown}, nil
}
return &GRPCTransportCredentials{s2aTransportCreds, config.s2aMTLSEndpoint, TransportTypeMTLSS2A}, nil
}
// GetHTTPTransportConfig returns a client certificate source and a function for
// dialing MTLS with S2A.
func GetHTTPTransportConfig(opts *Options) (cert.Provider, func(context.Context, string, string) (net.Conn, error), error) {
config, err := getTransportConfig(opts)
if err != nil {
return nil, nil, err
}
var s2aAddr string
var transportCredsForS2A credentials.TransportCredentials
if config.mtlsS2AAddress != "" {
s2aAddr = config.mtlsS2AAddress
transportCredsForS2A, err = loadMTLSMDSTransportCreds(mtlsMDSRoot, mtlsMDSKey)
if err != nil {
log.Printf("Loading MTLS MDS credentials failed: %v", err)
if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return config.clientCertSource, nil, nil
}
}
} else if config.s2aAddress != "" {
s2aAddr = config.s2aAddress
} else {
return config.clientCertSource, nil, nil
}
dialTLSContextFunc := s2a.NewS2ADialTLSContextFunc(&s2a.ClientOptions{
S2AAddress: s2aAddr,
TransportCreds: transportCredsForS2A,
})
return nil, dialTLSContextFunc, nil
}
func loadMTLSMDSTransportCreds(mtlsMDSRootFile, mtlsMDSKeyFile string) (credentials.TransportCredentials, error) {
rootPEM, err := os.ReadFile(mtlsMDSRootFile)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(rootPEM)
if !ok {
return nil, errors.New("failed to load MTLS MDS root certificate")
}
// The mTLS MDS credentials are formatted as the concatenation of a PEM-encoded certificate chain
// followed by a PEM-encoded private key. For this reason, the concatenation is passed in to the
// tls.X509KeyPair function as both the certificate chain and private key arguments.
cert, err := tls.LoadX509KeyPair(mtlsMDSKeyFile, mtlsMDSKeyFile)
if err != nil {
return nil, err
}
tlsConfig := tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
}
return credentials.NewTLS(&tlsConfig), nil
}
func getTransportConfig(opts *Options) (*transportConfig, error) {
clientCertSource, err := GetClientCertificateProvider(opts)
if err != nil {
return nil, err
}
endpoint, err := getEndpoint(opts, clientCertSource)
if err != nil {
return nil, err
}
defaultTransportConfig := transportConfig{
clientCertSource: clientCertSource,
endpoint: endpoint,
}
if !shouldUseS2A(clientCertSource, opts) {
return &defaultTransportConfig, nil
}
s2aAddress := GetS2AAddress(opts.Logger)
mtlsS2AAddress := GetMTLSS2AAddress(opts.Logger)
if s2aAddress == "" && mtlsS2AAddress == "" {
return &defaultTransportConfig, nil
}
return &transportConfig{
clientCertSource: clientCertSource,
endpoint: endpoint,
s2aAddress: s2aAddress,
mtlsS2AAddress: mtlsS2AAddress,
s2aMTLSEndpoint: opts.defaultMTLSEndpoint(),
}, nil
}
// GetClientCertificateProvider returns a default client certificate source, if
// not provided by the user.
//
// A nil default source can be returned if the source does not exist. Any exceptions
// encountered while initializing the default source will be reported as client
// error (ex. corrupt metadata file).
func GetClientCertificateProvider(opts *Options) (cert.Provider, error) {
if !isClientCertificateEnabled(opts) {
return nil, nil
} else if opts.ClientCertProvider != nil {
return opts.ClientCertProvider, nil
}
return cert.DefaultProvider()
}
// isClientCertificateEnabled returns true by default for all GDU universe domain, unless explicitly overridden by env var
func isClientCertificateEnabled(opts *Options) bool {
if value, ok := os.LookupEnv(googleAPIUseCertSource); ok {
// error as false is OK
b, _ := strconv.ParseBool(value)
return b
}
return opts.isUniverseDomainGDU()
}
type transportConfig struct {
// The client certificate source.
clientCertSource cert.Provider
// The corresponding endpoint to use based on client certificate source.
endpoint string
// The plaintext S2A address if it can be used, otherwise an empty string.
s2aAddress string
// The MTLS S2A address if it can be used, otherwise an empty string.
mtlsS2AAddress string
// The MTLS endpoint to use with S2A.
s2aMTLSEndpoint string
}
// getEndpoint returns the endpoint for the service, taking into account the
// user-provided endpoint override "settings.Endpoint".
//
// If no endpoint override is specified, we will either return the default
// endpoint or the default mTLS endpoint if a client certificate is available.
//
// You can override the default endpoint choice (mTLS vs. regular) by setting
// the GOOGLE_API_USE_MTLS_ENDPOINT environment variable.
//
// If the endpoint override is an address (host:port) rather than full base
// URL (ex. https://...), then the user-provided address will be merged into
// the default endpoint. For example, WithEndpoint("myhost:8000") and
// DefaultEndpointTemplate("https://UNIVERSE_DOMAIN/bar/baz") will return
// "https://myhost:8080/bar/baz". Note that this does not apply to the mTLS
// endpoint.
func getEndpoint(opts *Options, clientCertSource cert.Provider) (string, error) {
if opts.Endpoint == "" {
mtlsMode := getMTLSMode()
if mtlsMode == mTLSModeAlways || (clientCertSource != nil && mtlsMode == mTLSModeAuto) {
return opts.defaultMTLSEndpoint(), nil
}
return opts.defaultEndpoint(), nil
}
if strings.Contains(opts.Endpoint, "://") {
// User passed in a full URL path, use it verbatim.
return opts.Endpoint, nil
}
if opts.defaultEndpoint() == "" {
// If DefaultEndpointTemplate is not configured,
// use the user provided endpoint verbatim. This allows a naked
// "host[:port]" URL to be used with GRPC Direct Path.
return opts.Endpoint, nil
}
// Assume user-provided endpoint is host[:port], merge it with the default endpoint.
return opts.mergedEndpoint()
}
func getMTLSMode() string {
mode := os.Getenv(googleAPIUseMTLS)
if mode == "" {
mode = os.Getenv(googleAPIUseMTLSOld) // Deprecated.
}
if mode == "" {
return mTLSModeAuto
}
return strings.ToLower(mode)
}

View File

@@ -0,0 +1,65 @@
// 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 cert
import (
"crypto/tls"
"errors"
"sync"
)
// defaultCertData holds all the variables pertaining to
// the default certificate provider created by [DefaultProvider].
//
// A singleton model is used to allow the provider to be reused
// by the transport layer. As mentioned in [DefaultProvider] (provider nil, nil)
// may be returned to indicate a default provider could not be found, which
// will skip extra tls config in the transport layer .
type defaultCertData struct {
once sync.Once
provider Provider
err error
}
var (
defaultCert defaultCertData
)
// Provider is a function that can be passed into crypto/tls.Config.GetClientCertificate.
type Provider func(*tls.CertificateRequestInfo) (*tls.Certificate, error)
// errSourceUnavailable is a sentinel error to indicate certificate source is unavailable.
var errSourceUnavailable = errors.New("certificate source is unavailable")
// DefaultProvider returns a certificate source using the preferred EnterpriseCertificateProxySource.
// If EnterpriseCertificateProxySource is not available, fall back to the legacy SecureConnectSource.
//
// If neither source is available (due to missing configurations), a nil Source and a nil Error are
// returned to indicate that a default certificate source is unavailable.
func DefaultProvider() (Provider, error) {
defaultCert.once.Do(func() {
defaultCert.provider, defaultCert.err = NewWorkloadX509CertProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = NewEnterpriseCertificateProxyProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = NewSecureConnectProvider("")
if errors.Is(defaultCert.err, errSourceUnavailable) {
defaultCert.provider, defaultCert.err = nil, nil
}
}
}
})
return defaultCert.provider, defaultCert.err
}

View File

@@ -0,0 +1,54 @@
// 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 cert
import (
"crypto/tls"
"github.com/googleapis/enterprise-certificate-proxy/client"
)
type ecpSource struct {
key *client.Key
}
// NewEnterpriseCertificateProxyProvider creates a certificate source
// using the Enterprise Certificate Proxy client, which delegates
// certifcate related operations to an OS-specific "signer binary"
// that communicates with the native keystore (ex. keychain on MacOS).
//
// The configFilePath points to a config file containing relevant parameters
// such as the certificate issuer and the location of the signer binary.
// If configFilePath is empty, the client will attempt to load the config from
// a well-known gcloud location.
func NewEnterpriseCertificateProxyProvider(configFilePath string) (Provider, error) {
key, err := client.Cred(configFilePath)
if err != nil {
// TODO(codyoss): once this is fixed upstream can handle this error a
// little better here. But be safe for now and assume unavailable.
return nil, errSourceUnavailable
}
return (&ecpSource{
key: key,
}).getClientCertificate, nil
}
func (s *ecpSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
var cert tls.Certificate
cert.PrivateKey = s.key
cert.Certificate = s.key.CertificateChain()
return &cert, nil
}

View File

@@ -0,0 +1,124 @@
// 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 cert
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"sync"
"time"
)
const (
metadataPath = ".secureConnect"
metadataFile = "context_aware_metadata.json"
)
type secureConnectSource struct {
metadata secureConnectMetadata
// Cache the cert to avoid executing helper command repeatedly.
cachedCertMutex sync.Mutex
cachedCert *tls.Certificate
}
type secureConnectMetadata struct {
Cmd []string `json:"cert_provider_command"`
}
// NewSecureConnectProvider creates a certificate source using
// the Secure Connect Helper and its associated metadata file.
//
// The configFilePath points to the location of the context aware metadata file.
// If configFilePath is empty, use the default context aware metadata location.
func NewSecureConnectProvider(configFilePath string) (Provider, error) {
if configFilePath == "" {
user, err := user.Current()
if err != nil {
// Error locating the default config means Secure Connect is not supported.
return nil, errSourceUnavailable
}
configFilePath = filepath.Join(user.HomeDir, metadataPath, metadataFile)
}
file, err := os.ReadFile(configFilePath)
if err != nil {
// Config file missing means Secure Connect is not supported.
// There are non-os.ErrNotExist errors that may be returned.
// (e.g. if the home directory is /dev/null, *nix systems will
// return ENOTDIR instead of ENOENT)
return nil, errSourceUnavailable
}
var metadata secureConnectMetadata
if err := json.Unmarshal(file, &metadata); err != nil {
return nil, fmt.Errorf("cert: could not parse JSON in %q: %w", configFilePath, err)
}
if err := validateMetadata(metadata); err != nil {
return nil, fmt.Errorf("cert: invalid config in %q: %w", configFilePath, err)
}
return (&secureConnectSource{
metadata: metadata,
}).getClientCertificate, nil
}
func validateMetadata(metadata secureConnectMetadata) error {
if len(metadata.Cmd) == 0 {
return errors.New("empty cert_provider_command")
}
return nil
}
func (s *secureConnectSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
s.cachedCertMutex.Lock()
defer s.cachedCertMutex.Unlock()
if s.cachedCert != nil && !isCertificateExpired(s.cachedCert) {
return s.cachedCert, nil
}
// Expand OS environment variables in the cert provider command such as "$HOME".
for i := 0; i < len(s.metadata.Cmd); i++ {
s.metadata.Cmd[i] = os.ExpandEnv(s.metadata.Cmd[i])
}
command := s.metadata.Cmd
data, err := exec.Command(command[0], command[1:]...).Output()
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(data, data)
if err != nil {
return nil, err
}
s.cachedCert = &cert
return &cert, nil
}
// isCertificateExpired returns true if the given cert is expired or invalid.
func isCertificateExpired(cert *tls.Certificate) bool {
if len(cert.Certificate) == 0 {
return true
}
parsed, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return true
}
return time.Now().After(parsed.NotAfter)
}

View File

@@ -0,0 +1,138 @@
// Copyright 2024 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 cert
import (
"crypto/tls"
"encoding/json"
"errors"
"io"
"os"
"github.com/googleapis/enterprise-certificate-proxy/client/util"
)
type certConfigs struct {
Workload *workloadSource `json:"workload"`
}
type workloadSource struct {
CertPath string `json:"cert_path"`
KeyPath string `json:"key_path"`
}
type certificateConfig struct {
CertConfigs certConfigs `json:"cert_configs"`
}
// getconfigFilePath determines the path to the certificate configuration file.
// It first checks for the presence of an environment variable that specifies
// the file path. If the environment variable is not set, it falls back to
// a default configuration file path.
func getconfigFilePath() string {
envFilePath := util.GetConfigFilePathFromEnv()
if envFilePath != "" {
return envFilePath
}
return util.GetDefaultConfigFilePath()
}
// GetCertificatePath retrieves the certificate file path from the provided
// configuration file. If the configFilePath is empty, it attempts to load
// the configuration from a well-known gcloud location.
// This function is exposed to allow other packages, such as the
// externalaccount package, to retrieve the certificate path without needing
// to load the entire certificate configuration.
func GetCertificatePath(configFilePath string) (string, error) {
if configFilePath == "" {
configFilePath = getconfigFilePath()
}
certFile, _, err := getCertAndKeyFiles(configFilePath)
if err != nil {
return "", err
}
return certFile, nil
}
// NewWorkloadX509CertProvider creates a certificate source
// that reads a certificate and private key file from the local file system.
// This is intended to be used for workload identity federation.
//
// The configFilePath points to a config file containing relevant parameters
// such as the certificate and key file paths.
// If configFilePath is empty, the client will attempt to load the config from
// a well-known gcloud location.
func NewWorkloadX509CertProvider(configFilePath string) (Provider, error) {
if configFilePath == "" {
configFilePath = getconfigFilePath()
}
certFile, keyFile, err := getCertAndKeyFiles(configFilePath)
if err != nil {
return nil, err
}
source := &workloadSource{
CertPath: certFile,
KeyPath: keyFile,
}
return source.getClientCertificate, nil
}
// getClientCertificate attempts to load the certificate and key from the files specified in the
// certificate config.
func (s *workloadSource) getClientCertificate(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
cert, err := tls.LoadX509KeyPair(s.CertPath, s.KeyPath)
if err != nil {
return nil, err
}
return &cert, nil
}
// getCertAndKeyFiles attempts to read the provided config file and return the certificate and private
// key file paths.
func getCertAndKeyFiles(configFilePath string) (string, string, error) {
jsonFile, err := os.Open(configFilePath)
if err != nil {
return "", "", errSourceUnavailable
}
byteValue, err := io.ReadAll(jsonFile)
if err != nil {
return "", "", err
}
var config certificateConfig
if err := json.Unmarshal(byteValue, &config); err != nil {
return "", "", err
}
if config.CertConfigs.Workload == nil {
return "", "", errSourceUnavailable
}
certFile := config.CertConfigs.Workload.CertPath
keyFile := config.CertConfigs.Workload.KeyPath
if certFile == "" {
return "", "", errors.New("certificate configuration is missing the certificate file location")
}
if keyFile == "" {
return "", "", errors.New("certificate configuration is missing the key file location")
}
return certFile, keyFile, nil
}

View File

@@ -0,0 +1,61 @@
// Copyright 2025 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 headers
import (
"net/http"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
)
// SetAuthHeader uses the provided token to set the Authorization and trust
// boundary headers on a request. If the token.Type is empty, the type is
// assumed to be Bearer.
func SetAuthHeader(token *auth.Token, req *http.Request) {
typ := token.Type
if typ == "" {
typ = internal.TokenTypeBearer
}
req.Header.Set("Authorization", typ+" "+token.Value)
if headerVal, setHeader := getTrustBoundaryHeader(token); setHeader {
req.Header.Set("x-allowed-locations", headerVal)
}
}
// SetAuthMetadata uses the provided token to set the Authorization and trust
// boundary metadata. If the token.Type is empty, the type is assumed to be
// Bearer.
func SetAuthMetadata(token *auth.Token, m map[string]string) {
typ := token.Type
if typ == "" {
typ = internal.TokenTypeBearer
}
m["authorization"] = typ + " " + token.Value
if headerVal, setHeader := getTrustBoundaryHeader(token); setHeader {
m["x-allowed-locations"] = headerVal
}
}
func getTrustBoundaryHeader(token *auth.Token) (val string, present bool) {
if data, ok := token.Metadata[internal.TrustBoundaryDataKey]; ok {
if tbd, ok := data.(internal.TrustBoundaryData); ok {
return tbd.TrustBoundaryHeader()
}
}
return "", false
}

View File

@@ -0,0 +1,138 @@
// 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 transport
import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"os"
"strconv"
"sync"
"cloud.google.com/go/auth/internal/transport/cert"
"cloud.google.com/go/compute/metadata"
)
const (
configEndpointSuffix = "instance/platform-security/auto-mtls-configuration"
)
var (
mtlsConfiguration *mtlsConfig
mtlsOnce sync.Once
)
// GetS2AAddress returns the S2A address to be reached via plaintext connection.
// Returns empty string if not set or invalid.
func GetS2AAddress(logger *slog.Logger) string {
getMetadataMTLSAutoConfig(logger)
if !mtlsConfiguration.valid() {
return ""
}
return mtlsConfiguration.S2A.PlaintextAddress
}
// GetMTLSS2AAddress returns the S2A address to be reached via MTLS connection.
// Returns empty string if not set or invalid.
func GetMTLSS2AAddress(logger *slog.Logger) string {
getMetadataMTLSAutoConfig(logger)
if !mtlsConfiguration.valid() {
return ""
}
return mtlsConfiguration.S2A.MTLSAddress
}
// mtlsConfig contains the configuration for establishing MTLS connections with Google APIs.
type mtlsConfig struct {
S2A *s2aAddresses `json:"s2a"`
}
func (c *mtlsConfig) valid() bool {
return c != nil && c.S2A != nil
}
// s2aAddresses contains the plaintext and/or MTLS S2A addresses.
type s2aAddresses struct {
// PlaintextAddress is the plaintext address to reach S2A
PlaintextAddress string `json:"plaintext_address"`
// MTLSAddress is the MTLS address to reach S2A
MTLSAddress string `json:"mtls_address"`
}
func getMetadataMTLSAutoConfig(logger *slog.Logger) {
var err error
mtlsOnce.Do(func() {
mtlsConfiguration, err = queryConfig(logger)
if err != nil {
log.Printf("Getting MTLS config failed: %v", err)
}
})
}
var httpGetMetadataMTLSConfig = func(logger *slog.Logger) (string, error) {
metadataClient := metadata.NewWithOptions(&metadata.Options{
Logger: logger,
})
return metadataClient.GetWithContext(context.Background(), configEndpointSuffix)
}
func queryConfig(logger *slog.Logger) (*mtlsConfig, error) {
resp, err := httpGetMetadataMTLSConfig(logger)
if err != nil {
return nil, fmt.Errorf("querying MTLS config from MDS endpoint failed: %w", err)
}
var config mtlsConfig
err = json.Unmarshal([]byte(resp), &config)
if err != nil {
return nil, fmt.Errorf("unmarshalling MTLS config from MDS endpoint failed: %w", err)
}
if config.S2A == nil {
return nil, fmt.Errorf("returned MTLS config from MDS endpoint is invalid: %v", config)
}
return &config, nil
}
func shouldUseS2A(clientCertSource cert.Provider, opts *Options) bool {
// If client cert is found, use that over S2A.
if clientCertSource != nil {
return false
}
// If EXPERIMENTAL_GOOGLE_API_USE_S2A is not set to true, skip S2A.
if !isGoogleS2AEnabled() {
return false
}
// If DefaultMTLSEndpoint is not set or has endpoint override, skip S2A.
if opts.DefaultMTLSEndpoint == "" || opts.Endpoint != "" {
return false
}
// If custom HTTP client is provided, skip S2A.
if opts.Client != nil {
return false
}
// If directPath is enabled, skip S2A.
return !opts.EnableDirectPath && !opts.EnableDirectPathXds
}
func isGoogleS2AEnabled() bool {
b, err := strconv.ParseBool(os.Getenv(googleAPIUseS2AEnv))
if err != nil {
return false
}
return b
}

View File

@@ -0,0 +1,107 @@
// 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 transport provided internal helpers for the two transport packages
// (grpctransport and httptransport).
package transport
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"time"
"cloud.google.com/go/auth/credentials"
)
// CloneDetectOptions clones a user set detect option into some new memory that
// we can internally manipulate before sending onto the detect package.
func CloneDetectOptions(oldDo *credentials.DetectOptions) *credentials.DetectOptions {
if oldDo == nil {
// it is valid for users not to set this, but we will need to to default
// some options for them in this case so return some initialized memory
// to work with.
return &credentials.DetectOptions{}
}
newDo := &credentials.DetectOptions{
// Simple types
TokenBindingType: oldDo.TokenBindingType,
Audience: oldDo.Audience,
Subject: oldDo.Subject,
EarlyTokenRefresh: oldDo.EarlyTokenRefresh,
TokenURL: oldDo.TokenURL,
STSAudience: oldDo.STSAudience,
CredentialsFile: oldDo.CredentialsFile,
UseSelfSignedJWT: oldDo.UseSelfSignedJWT,
UniverseDomain: oldDo.UniverseDomain,
// These fields are pointer types that we just want to use exactly as
// the user set, copy the ref
Client: oldDo.Client,
Logger: oldDo.Logger,
AuthHandlerOptions: oldDo.AuthHandlerOptions,
}
// Smartly size this memory and copy below.
if len(oldDo.CredentialsJSON) > 0 {
newDo.CredentialsJSON = make([]byte, len(oldDo.CredentialsJSON))
copy(newDo.CredentialsJSON, oldDo.CredentialsJSON)
}
if len(oldDo.Scopes) > 0 {
newDo.Scopes = make([]string, len(oldDo.Scopes))
copy(newDo.Scopes, oldDo.Scopes)
}
return newDo
}
// ValidateUniverseDomain verifies that the universe domain configured for the
// client matches the universe domain configured for the credentials.
func ValidateUniverseDomain(clientUniverseDomain, credentialsUniverseDomain string) error {
if clientUniverseDomain != credentialsUniverseDomain {
return fmt.Errorf(
"the configured universe domain (%q) does not match the universe "+
"domain found in the credentials (%q). If you haven't configured "+
"the universe domain explicitly, \"googleapis.com\" is the default",
clientUniverseDomain,
credentialsUniverseDomain)
}
return nil
}
// DefaultHTTPClientWithTLS constructs an HTTPClient using the provided tlsConfig, to support mTLS.
func DefaultHTTPClientWithTLS(tlsConfig *tls.Config) *http.Client {
trans := BaseTransport()
trans.TLSClientConfig = tlsConfig
return &http.Client{Transport: trans}
}
// BaseTransport returns a default [http.Transport] which can be used if
// [http.DefaultTransport] has been overwritten.
func BaseTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}

View File

@@ -0,0 +1,100 @@
// Copyright 2025 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 trustboundary
import (
"context"
"fmt"
"regexp"
)
const (
workloadAllowedLocationsEndpoint = "https://iamcredentials.%s/v1/projects/%s/locations/global/workloadIdentityPools/%s/allowedLocations"
workforceAllowedLocationsEndpoint = "https://iamcredentials.%s/v1/locations/global/workforcePools/%s/allowedLocations"
)
var (
workforceAudiencePattern = regexp.MustCompile(`//iam\.([^/]+)/locations/global/workforcePools/([^/]+)`)
workloadAudiencePattern = regexp.MustCompile(`//iam\.([^/]+)/projects/([^/]+)/locations/global/workloadIdentityPools/([^/]+)`)
)
// NewExternalAccountConfigProvider creates a new ConfigProvider for external accounts.
func NewExternalAccountConfigProvider(audience, inputUniverseDomain string) (ConfigProvider, error) {
var audienceDomain, projectNumber, poolID string
var isWorkload bool
matches := workloadAudiencePattern.FindStringSubmatch(audience)
if len(matches) == 4 { // Expecting full match, domain, projectNumber, poolID
audienceDomain = matches[1]
projectNumber = matches[2]
poolID = matches[3]
isWorkload = true
} else {
matches = workforceAudiencePattern.FindStringSubmatch(audience)
if len(matches) == 3 { // Expecting full match, domain, poolID
audienceDomain = matches[1]
poolID = matches[2]
isWorkload = false
} else {
return nil, fmt.Errorf("trustboundary: unknown audience format: %q", audience)
}
}
effectiveUniverseDomain := inputUniverseDomain
if effectiveUniverseDomain == "" {
effectiveUniverseDomain = audienceDomain
} else if audienceDomain != "" && effectiveUniverseDomain != audienceDomain {
return nil, fmt.Errorf("trustboundary: provided universe domain (%q) does not match domain in audience (%q)", inputUniverseDomain, audienceDomain)
}
if isWorkload {
return &workloadIdentityPoolConfigProvider{
projectNumber: projectNumber,
poolID: poolID,
universeDomain: effectiveUniverseDomain,
}, nil
}
return &workforcePoolConfigProvider{
poolID: poolID,
universeDomain: effectiveUniverseDomain,
}, nil
}
type workforcePoolConfigProvider struct {
poolID string
universeDomain string
}
func (p *workforcePoolConfigProvider) GetTrustBoundaryEndpoint(ctx context.Context) (string, error) {
return fmt.Sprintf(workforceAllowedLocationsEndpoint, p.universeDomain, p.poolID), nil
}
func (p *workforcePoolConfigProvider) GetUniverseDomain(ctx context.Context) (string, error) {
return p.universeDomain, nil
}
type workloadIdentityPoolConfigProvider struct {
projectNumber string
poolID string
universeDomain string
}
func (p *workloadIdentityPoolConfigProvider) GetTrustBoundaryEndpoint(ctx context.Context) (string, error) {
return fmt.Sprintf(workloadAllowedLocationsEndpoint, p.universeDomain, p.projectNumber, p.poolID), nil
}
func (p *workloadIdentityPoolConfigProvider) GetUniverseDomain(ctx context.Context) (string, error) {
return p.universeDomain, nil
}

View File

@@ -0,0 +1,392 @@
// Copyright 2025 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 trustboundary
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"strings"
"sync"
"cloud.google.com/go/auth"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/retry"
"cloud.google.com/go/auth/internal/transport/headers"
"github.com/googleapis/gax-go/v2/internallog"
)
const (
// serviceAccountAllowedLocationsEndpoint is the URL for fetching allowed locations for a given service account email.
serviceAccountAllowedLocationsEndpoint = "https://iamcredentials.%s/v1/projects/-/serviceAccounts/%s/allowedLocations"
)
// isEnabled wraps isTrustBoundaryEnabled with sync.OnceValues to ensure it's
// called only once.
var isEnabled = sync.OnceValues(isTrustBoundaryEnabled)
// IsEnabled returns if the trust boundary feature is enabled and an error if
// the configuration is invalid. The underlying check is performed only once.
func IsEnabled() (bool, error) {
return isEnabled()
}
// isTrustBoundaryEnabled checks if the trust boundary feature is enabled via
// GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED environment variable.
//
// If the environment variable is not set, it is considered false.
//
// The environment variable is interpreted as a boolean with the following
// (case-insensitive) rules:
// - "true", "1" are considered true.
// - "false", "0" are considered false.
//
// Any other values will return an error.
func isTrustBoundaryEnabled() (bool, error) {
const envVar = "GOOGLE_AUTH_TRUST_BOUNDARY_ENABLED"
val, ok := os.LookupEnv(envVar)
if !ok {
return false, nil
}
val = strings.ToLower(val)
switch val {
case "true", "1":
return true, nil
case "false", "0":
return false, nil
default:
return false, fmt.Errorf(`invalid value for %s: %q. Must be one of "true", "false", "1", or "0"`, envVar, val)
}
}
// ConfigProvider provides specific configuration for trust boundary lookups.
type ConfigProvider interface {
// GetTrustBoundaryEndpoint returns the endpoint URL for the trust boundary lookup.
GetTrustBoundaryEndpoint(ctx context.Context) (url string, err error)
// GetUniverseDomain returns the universe domain associated with the credential.
// It may return an error if the universe domain cannot be determined.
GetUniverseDomain(ctx context.Context) (string, error)
}
// AllowedLocationsResponse is the structure of the response from the Trust Boundary API.
type AllowedLocationsResponse struct {
// Locations is the list of allowed locations.
Locations []string `json:"locations"`
// EncodedLocations is the encoded representation of the allowed locations.
EncodedLocations string `json:"encodedLocations"`
}
// fetchTrustBoundaryData fetches the trust boundary data from the API.
func fetchTrustBoundaryData(ctx context.Context, client *http.Client, url string, token *auth.Token, logger *slog.Logger) (*internal.TrustBoundaryData, error) {
if logger == nil {
logger = slog.New(slog.NewTextHandler(io.Discard, nil))
}
if client == nil {
return nil, errors.New("trustboundary: HTTP client is required")
}
if url == "" {
return nil, errors.New("trustboundary: URL cannot be empty")
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("trustboundary: failed to create trust boundary request: %w", err)
}
if token == nil || token.Value == "" {
return nil, errors.New("trustboundary: access token required for lookup API authentication")
}
headers.SetAuthHeader(token, req)
logger.DebugContext(ctx, "trust boundary request", "request", internallog.HTTPRequest(req, nil))
retryer := retry.New()
var response *http.Response
for {
response, err = client.Do(req)
var statusCode int
if response != nil {
statusCode = response.StatusCode
}
pause, shouldRetry := retryer.Retry(statusCode, err)
if !shouldRetry {
break
}
if response != nil {
// Drain and close the body to reuse the connection
io.Copy(io.Discard, response.Body)
response.Body.Close()
}
if err := retry.Sleep(ctx, pause); err != nil {
return nil, err
}
}
if err != nil {
return nil, fmt.Errorf("trustboundary: failed to fetch trust boundary: %w", err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("trustboundary: failed to read trust boundary response: %w", err)
}
logger.DebugContext(ctx, "trust boundary response", "response", internallog.HTTPResponse(response, body))
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("trustboundary: trust boundary request failed with status: %s, body: %s", response.Status, string(body))
}
apiResponse := AllowedLocationsResponse{}
if err := json.Unmarshal(body, &apiResponse); err != nil {
return nil, fmt.Errorf("trustboundary: failed to unmarshal trust boundary response: %w", err)
}
if apiResponse.EncodedLocations == "" {
return nil, errors.New("trustboundary: invalid API response: encodedLocations is empty")
}
return internal.NewTrustBoundaryData(apiResponse.Locations, apiResponse.EncodedLocations), nil
}
// serviceAccountConfig holds configuration for SA trust boundary lookups.
// It implements the ConfigProvider interface.
type serviceAccountConfig struct {
ServiceAccountEmail string
UniverseDomain string
}
// NewServiceAccountConfigProvider creates a new config for service accounts.
func NewServiceAccountConfigProvider(saEmail, universeDomain string) ConfigProvider {
return &serviceAccountConfig{
ServiceAccountEmail: saEmail,
UniverseDomain: universeDomain,
}
}
// GetTrustBoundaryEndpoint returns the formatted URL for fetching allowed locations
// for the configured service account and universe domain.
func (sac *serviceAccountConfig) GetTrustBoundaryEndpoint(ctx context.Context) (url string, err error) {
if sac.ServiceAccountEmail == "" {
return "", errors.New("trustboundary: service account email cannot be empty for config")
}
ud := sac.UniverseDomain
if ud == "" {
ud = internal.DefaultUniverseDomain
}
return fmt.Sprintf(serviceAccountAllowedLocationsEndpoint, ud, sac.ServiceAccountEmail), nil
}
// GetUniverseDomain returns the configured universe domain, defaulting to
// [internal.DefaultUniverseDomain] if not explicitly set.
func (sac *serviceAccountConfig) GetUniverseDomain(ctx context.Context) (string, error) {
if sac.UniverseDomain == "" {
return internal.DefaultUniverseDomain, nil
}
return sac.UniverseDomain, nil
}
// DataProvider fetches and caches trust boundary Data.
// It implements the DataProvider interface and uses a ConfigProvider
// to get type-specific details for the lookup.
type DataProvider struct {
client *http.Client
configProvider ConfigProvider
data *internal.TrustBoundaryData
logger *slog.Logger
base auth.TokenProvider
}
// NewProvider wraps the provided base [auth.TokenProvider] to create a new
// provider that injects tokens with trust boundary data. It uses the provided
// HTTP client and configProvider to fetch the data and attach it to the token's
// metadata.
func NewProvider(client *http.Client, configProvider ConfigProvider, logger *slog.Logger, base auth.TokenProvider) (*DataProvider, error) {
if client == nil {
return nil, errors.New("trustboundary: HTTP client cannot be nil for DataProvider")
}
if configProvider == nil {
return nil, errors.New("trustboundary: ConfigProvider cannot be nil for DataProvider")
}
p := &DataProvider{
client: client,
configProvider: configProvider,
logger: internallog.New(logger),
base: base,
}
return p, nil
}
// Token retrieves a token from the base provider and injects it with trust
// boundary data.
func (p *DataProvider) Token(ctx context.Context) (*auth.Token, error) {
// Get the original token.
token, err := p.base.Token(ctx)
if err != nil {
return nil, err
}
tbData, err := p.GetTrustBoundaryData(ctx, token)
if err != nil {
return nil, fmt.Errorf("trustboundary: error fetching the trust boundary data: %w", err)
}
if tbData != nil {
if token.Metadata == nil {
token.Metadata = make(map[string]interface{})
}
token.Metadata[internal.TrustBoundaryDataKey] = *tbData
}
return token, nil
}
// GetTrustBoundaryData retrieves the trust boundary data.
// It first checks the universe domain: if it's non-default, a NoOp is returned.
// Otherwise, it checks a local cache. If the data is not cached as NoOp,
// it fetches new data from the endpoint provided by its ConfigProvider,
// using the given accessToken for authentication. Results are cached.
// If fetching fails, it returns previously cached data if available, otherwise the fetch error.
func (p *DataProvider) GetTrustBoundaryData(ctx context.Context, token *auth.Token) (*internal.TrustBoundaryData, error) {
// Check the universe domain.
uniDomain, err := p.configProvider.GetUniverseDomain(ctx)
if err != nil {
return nil, fmt.Errorf("trustboundary: error getting universe domain: %w", err)
}
if uniDomain != "" && uniDomain != internal.DefaultUniverseDomain {
if p.data == nil || p.data.EncodedLocations != internal.TrustBoundaryNoOp {
p.data = internal.NewNoOpTrustBoundaryData()
}
return p.data, nil
}
// Check cache for a no-op result from a previous API call.
cachedData := p.data
if cachedData != nil && cachedData.EncodedLocations == internal.TrustBoundaryNoOp {
return cachedData, nil
}
// Get the endpoint
url, err := p.configProvider.GetTrustBoundaryEndpoint(ctx)
if err != nil {
return nil, fmt.Errorf("trustboundary: error getting the lookup endpoint: %w", err)
}
// Proceed to fetch new data.
newData, fetchErr := fetchTrustBoundaryData(ctx, p.client, url, token, p.logger)
if fetchErr != nil {
// Fetch failed. Fallback to cachedData if available.
if cachedData != nil {
return cachedData, nil // Successful fallback
}
// No cache to fallback to.
return nil, fmt.Errorf("trustboundary: failed to fetch trust boundary data for endpoint %s and no cache available: %w", url, fetchErr)
}
// Fetch successful. Update cache.
p.data = newData
return newData, nil
}
// GCEConfigProvider implements ConfigProvider for GCE environments.
// It lazily fetches and caches the necessary metadata (service account email, universe domain)
// from the GCE metadata server.
type GCEConfigProvider struct {
// universeDomainProvider provides the universe domain and underlying metadata client.
universeDomainProvider *internal.ComputeUniverseDomainProvider
// Caching for service account email
saOnce sync.Once
saEmail string
saEmailErr error
// Caching for universe domain
udOnce sync.Once
ud string
udErr error
}
// NewGCEConfigProvider creates a new GCEConfigProvider
// which uses the provided gceUDP to interact with the GCE metadata server.
func NewGCEConfigProvider(gceUDP *internal.ComputeUniverseDomainProvider) *GCEConfigProvider {
// The validity of gceUDP and its internal MetadataClient will be checked
// within the GetTrustBoundaryEndpoint and GetUniverseDomain methods.
return &GCEConfigProvider{
universeDomainProvider: gceUDP,
}
}
func (g *GCEConfigProvider) fetchSA(ctx context.Context) {
if g.universeDomainProvider == nil || g.universeDomainProvider.MetadataClient == nil {
g.saEmailErr = errors.New("trustboundary: GCEConfigProvider not properly initialized (missing ComputeUniverseDomainProvider or MetadataClient)")
return
}
mdClient := g.universeDomainProvider.MetadataClient
saEmail, err := mdClient.EmailWithContext(ctx, "default")
if err != nil {
g.saEmailErr = fmt.Errorf("trustboundary: GCE config: failed to get service account email: %w", err)
return
}
g.saEmail = saEmail
}
func (g *GCEConfigProvider) fetchUD(ctx context.Context) {
if g.universeDomainProvider == nil || g.universeDomainProvider.MetadataClient == nil {
g.udErr = errors.New("trustboundary: GCEConfigProvider not properly initialized (missing ComputeUniverseDomainProvider or MetadataClient)")
return
}
ud, err := g.universeDomainProvider.GetProperty(ctx)
if err != nil {
g.udErr = fmt.Errorf("trustboundary: GCE config: failed to get universe domain: %w", err)
return
}
if ud == "" {
ud = internal.DefaultUniverseDomain
}
g.ud = ud
}
// GetTrustBoundaryEndpoint constructs the trust boundary lookup URL for a GCE environment.
// It uses cached metadata (service account email, universe domain) after the first call.
func (g *GCEConfigProvider) GetTrustBoundaryEndpoint(ctx context.Context) (string, error) {
g.saOnce.Do(func() { g.fetchSA(ctx) })
if g.saEmailErr != nil {
return "", g.saEmailErr
}
g.udOnce.Do(func() { g.fetchUD(ctx) })
if g.udErr != nil {
return "", g.udErr
}
return fmt.Sprintf(serviceAccountAllowedLocationsEndpoint, g.ud, g.saEmail), nil
}
// GetUniverseDomain retrieves the universe domain from the GCE metadata server.
// It uses a cached value after the first call.
func (g *GCEConfigProvider) GetUniverseDomain(ctx context.Context) (string, error) {
g.udOnce.Do(func() { g.fetchUD(ctx) })
if g.udErr != nil {
return "", g.udErr
}
return g.ud, nil
}

20
vendor/cloud.google.com/go/auth/internal/version.go generated vendored Normal file
View File

@@ -0,0 +1,20 @@
// Copyright 2026 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.
// Code generated by gapicgen. DO NOT EDIT.
package internal
// Version is the current tagged release of the library.
const Version = "0.18.1"