| // 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 | |
| } |