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