url_provider.go

 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
15package externalaccount
16
17import (
18	"context"
19	"encoding/json"
20	"errors"
21	"fmt"
22	"log/slog"
23	"net/http"
24
25	"cloud.google.com/go/auth/internal"
26	"cloud.google.com/go/auth/internal/credsfile"
27	"github.com/googleapis/gax-go/v2/internallog"
28)
29
30const (
31	fileTypeText             = "text"
32	fileTypeJSON             = "json"
33	urlProviderType          = "url"
34	programmaticProviderType = "programmatic"
35	x509ProviderType         = "x509"
36)
37
38type urlSubjectProvider struct {
39	URL     string
40	Headers map[string]string
41	Format  *credsfile.Format
42	Client  *http.Client
43	Logger  *slog.Logger
44}
45
46func (sp *urlSubjectProvider) subjectToken(ctx context.Context) (string, error) {
47	req, err := http.NewRequestWithContext(ctx, "GET", sp.URL, nil)
48	if err != nil {
49		return "", fmt.Errorf("credentials: HTTP request for URL-sourced credential failed: %w", err)
50	}
51
52	for key, val := range sp.Headers {
53		req.Header.Add(key, val)
54	}
55	sp.Logger.DebugContext(ctx, "url subject token request", "request", internallog.HTTPRequest(req, nil))
56	resp, body, err := internal.DoRequest(sp.Client, req)
57	if err != nil {
58		return "", fmt.Errorf("credentials: invalid response when retrieving subject token: %w", err)
59	}
60	sp.Logger.DebugContext(ctx, "url subject token response", "response", internallog.HTTPResponse(resp, body))
61	if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
62		return "", fmt.Errorf("credentials: status code %d: %s", c, body)
63	}
64
65	if sp.Format == nil {
66		return string(body), nil
67	}
68	switch sp.Format.Type {
69	case "json":
70		jsonData := make(map[string]interface{})
71		err = json.Unmarshal(body, &jsonData)
72		if err != nil {
73			return "", fmt.Errorf("credentials: failed to unmarshal subject token file: %w", err)
74		}
75		val, ok := jsonData[sp.Format.SubjectTokenFieldName]
76		if !ok {
77			return "", errors.New("credentials: provided subject_token_field_name not found in credentials")
78		}
79		token, ok := val.(string)
80		if !ok {
81			return "", errors.New("credentials: improperly formatted subject token")
82		}
83		return token, nil
84	case fileTypeText:
85		return string(body), nil
86	default:
87		return "", errors.New("credentials: invalid credential_source file format type: " + sp.Format.Type)
88	}
89}
90
91func (sp *urlSubjectProvider) providerType() string {
92	return urlProviderType
93}