internal.go raw

   1  // Copyright 2023 Google LLC
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  package internal
  16  
  17  import (
  18  	"context"
  19  	"crypto"
  20  	"crypto/x509"
  21  	"encoding/json"
  22  	"encoding/pem"
  23  	"errors"
  24  	"fmt"
  25  	"io"
  26  	"net/http"
  27  	"os"
  28  	"sync"
  29  	"time"
  30  
  31  	"cloud.google.com/go/compute/metadata"
  32  )
  33  
  34  const (
  35  	// TokenTypeBearer is the auth header prefix for bearer tokens.
  36  	TokenTypeBearer = "Bearer"
  37  
  38  	// QuotaProjectEnvVar is the environment variable for setting the quota
  39  	// project.
  40  	QuotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
  41  	// UniverseDomainEnvVar is the environment variable for setting the default
  42  	// service domain for a given Cloud universe.
  43  	UniverseDomainEnvVar = "GOOGLE_CLOUD_UNIVERSE_DOMAIN"
  44  	projectEnvVar        = "GOOGLE_CLOUD_PROJECT"
  45  	maxBodySize          = 1 << 20
  46  
  47  	// DefaultUniverseDomain is the default value for universe domain.
  48  	// Universe domain is the default service domain for a given Cloud universe.
  49  	DefaultUniverseDomain = "googleapis.com"
  50  
  51  	// TrustBoundaryNoOp is a constant indicating no trust boundary is enforced.
  52  	TrustBoundaryNoOp = "0x0"
  53  
  54  	// TrustBoundaryDataKey is the key used to store trust boundary data in a token's metadata.
  55  	TrustBoundaryDataKey = "google.auth.trust_boundary_data"
  56  )
  57  
  58  type clonableTransport interface {
  59  	Clone() *http.Transport
  60  }
  61  
  62  // DefaultClient returns an [http.Client] with some defaults set. If
  63  // the current [http.DefaultTransport] is a [clonableTransport], as
  64  // is the case for an [*http.Transport], the clone will be used.
  65  // Otherwise the [http.DefaultTransport] is used directly.
  66  func DefaultClient() *http.Client {
  67  	if transport, ok := http.DefaultTransport.(clonableTransport); ok {
  68  		return &http.Client{
  69  			Transport: transport.Clone(),
  70  			Timeout:   30 * time.Second,
  71  		}
  72  	}
  73  
  74  	return &http.Client{
  75  		Transport: http.DefaultTransport,
  76  		Timeout:   30 * time.Second,
  77  	}
  78  }
  79  
  80  // ParseKey converts the binary contents of a private key file
  81  // to an crypto.Signer. It detects whether the private key is in a
  82  // PEM container or not. If so, it extracts the the private key
  83  // from PEM container before conversion. It only supports PEM
  84  // containers with no passphrase.
  85  func ParseKey(key []byte) (crypto.Signer, error) {
  86  	block, _ := pem.Decode(key)
  87  	if block != nil {
  88  		key = block.Bytes
  89  	}
  90  	var parsedKey crypto.PrivateKey
  91  
  92  	var errPKCS8, errPKCS1, errEC error
  93  	if parsedKey, errPKCS8 = x509.ParsePKCS8PrivateKey(key); errPKCS8 != nil {
  94  		if parsedKey, errPKCS1 = x509.ParsePKCS1PrivateKey(key); errPKCS1 != nil {
  95  			if parsedKey, errEC = x509.ParseECPrivateKey(key); errEC != nil {
  96  				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)
  97  			}
  98  		}
  99  	}
 100  	parsed, ok := parsedKey.(crypto.Signer)
 101  	if !ok {
 102  		return nil, errors.New("private key is not a signer")
 103  	}
 104  	return parsed, nil
 105  }
 106  
 107  // GetQuotaProject retrieves quota project with precedence being: override,
 108  // environment variable, creds json file.
 109  func GetQuotaProject(b []byte, override string) string {
 110  	if override != "" {
 111  		return override
 112  	}
 113  	if env := os.Getenv(QuotaProjectEnvVar); env != "" {
 114  		return env
 115  	}
 116  	if b == nil {
 117  		return ""
 118  	}
 119  	var v struct {
 120  		QuotaProject string `json:"quota_project_id"`
 121  	}
 122  	if err := json.Unmarshal(b, &v); err != nil {
 123  		return ""
 124  	}
 125  	return v.QuotaProject
 126  }
 127  
 128  // GetProjectID retrieves project with precedence being: override,
 129  // environment variable, creds json file.
 130  func GetProjectID(b []byte, override string) string {
 131  	if override != "" {
 132  		return override
 133  	}
 134  	if env := os.Getenv(projectEnvVar); env != "" {
 135  		return env
 136  	}
 137  	if b == nil {
 138  		return ""
 139  	}
 140  	var v struct {
 141  		ProjectID string `json:"project_id"` // standard service account key
 142  		Project   string `json:"project"`    // gdch key
 143  	}
 144  	if err := json.Unmarshal(b, &v); err != nil {
 145  		return ""
 146  	}
 147  	if v.ProjectID != "" {
 148  		return v.ProjectID
 149  	}
 150  	return v.Project
 151  }
 152  
 153  // DoRequest executes the provided req with the client. It reads the response
 154  // body, closes it, and returns it.
 155  func DoRequest(client *http.Client, req *http.Request) (*http.Response, []byte, error) {
 156  	resp, err := client.Do(req)
 157  	if err != nil {
 158  		return nil, nil, err
 159  	}
 160  	defer resp.Body.Close()
 161  	body, err := ReadAll(io.LimitReader(resp.Body, maxBodySize))
 162  	if err != nil {
 163  		return nil, nil, err
 164  	}
 165  	return resp, body, nil
 166  }
 167  
 168  // ReadAll consumes the whole reader and safely reads the content of its body
 169  // with some overflow protection.
 170  func ReadAll(r io.Reader) ([]byte, error) {
 171  	return io.ReadAll(io.LimitReader(r, maxBodySize))
 172  }
 173  
 174  // StaticCredentialsProperty is a helper for creating static credentials
 175  // properties.
 176  func StaticCredentialsProperty(s string) StaticProperty {
 177  	return StaticProperty(s)
 178  }
 179  
 180  // StaticProperty always returns that value of the underlying string.
 181  type StaticProperty string
 182  
 183  // GetProperty loads the properly value provided the given context.
 184  func (p StaticProperty) GetProperty(context.Context) (string, error) {
 185  	return string(p), nil
 186  }
 187  
 188  // ComputeUniverseDomainProvider fetches the credentials universe domain from
 189  // the google cloud metadata service.
 190  type ComputeUniverseDomainProvider struct {
 191  	MetadataClient     *metadata.Client
 192  	universeDomainOnce sync.Once
 193  	universeDomain     string
 194  	universeDomainErr  error
 195  }
 196  
 197  // GetProperty fetches the credentials universe domain from the google cloud
 198  // metadata service.
 199  func (c *ComputeUniverseDomainProvider) GetProperty(ctx context.Context) (string, error) {
 200  	c.universeDomainOnce.Do(func() {
 201  		c.universeDomain, c.universeDomainErr = getMetadataUniverseDomain(ctx, c.MetadataClient)
 202  	})
 203  	if c.universeDomainErr != nil {
 204  		return "", c.universeDomainErr
 205  	}
 206  	return c.universeDomain, nil
 207  }
 208  
 209  // httpGetMetadataUniverseDomain is a package var for unit test substitution.
 210  var httpGetMetadataUniverseDomain = func(ctx context.Context, client *metadata.Client) (string, error) {
 211  	ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
 212  	defer cancel()
 213  	return client.GetWithContext(ctx, "universe/universe-domain")
 214  }
 215  
 216  func getMetadataUniverseDomain(ctx context.Context, client *metadata.Client) (string, error) {
 217  	universeDomain, err := httpGetMetadataUniverseDomain(ctx, client)
 218  	if err == nil {
 219  		return universeDomain, nil
 220  	}
 221  	if _, ok := err.(metadata.NotDefinedError); ok {
 222  		// http.StatusNotFound (404)
 223  		return DefaultUniverseDomain, nil
 224  	}
 225  	return "", err
 226  }
 227  
 228  // FormatIAMServiceAccountResource sets a service account name in an IAM resource
 229  // name.
 230  func FormatIAMServiceAccountResource(name string) string {
 231  	return fmt.Sprintf("projects/-/serviceAccounts/%s", name)
 232  }
 233  
 234  // TrustBoundaryData represents the trust boundary data associated with a token.
 235  // It contains information about the regions or environments where the token is valid.
 236  type TrustBoundaryData struct {
 237  	// Locations is the list of locations that the token is allowed to be used in.
 238  	Locations []string
 239  	// EncodedLocations represents the locations in an encoded format.
 240  	EncodedLocations string
 241  }
 242  
 243  // NewTrustBoundaryData returns a new TrustBoundaryData with the specified locations and encoded locations.
 244  func NewTrustBoundaryData(locations []string, encodedLocations string) *TrustBoundaryData {
 245  	// Ensure consistency by treating a nil slice as an empty slice.
 246  	if locations == nil {
 247  		locations = []string{}
 248  	}
 249  	locationsCopy := make([]string, len(locations))
 250  	copy(locationsCopy, locations)
 251  	return &TrustBoundaryData{
 252  		Locations:        locationsCopy,
 253  		EncodedLocations: encodedLocations,
 254  	}
 255  }
 256  
 257  // NewNoOpTrustBoundaryData returns a new TrustBoundaryData with no restrictions.
 258  func NewNoOpTrustBoundaryData() *TrustBoundaryData {
 259  	return &TrustBoundaryData{
 260  		Locations:        []string{},
 261  		EncodedLocations: TrustBoundaryNoOp,
 262  	}
 263  }
 264  
 265  // TrustBoundaryHeader returns the value for the x-allowed-locations header and a bool
 266  // indicating if the header should be set. The return values are structured to
 267  // handle three distinct states required by the backend:
 268  // 1. Header not set: (value="", present=false) -> data is empty.
 269  // 2. Header set to an empty string: (value="", present=true) -> data is a no-op.
 270  // 3. Header set to a value: (value="...", present=true) -> data has locations.
 271  func (t TrustBoundaryData) TrustBoundaryHeader() (value string, present bool) {
 272  	if t.EncodedLocations == "" {
 273  		// If the data is empty, the header should not be present.
 274  		return "", false
 275  	}
 276  
 277  	// If data is not empty, the header should always be present.
 278  	present = true
 279  	value = ""
 280  	if t.EncodedLocations != TrustBoundaryNoOp {
 281  		value = t.EncodedLocations
 282  	}
 283  	// For a no-op, the backend requires an empty string.
 284  	return value, present
 285  }
 286