1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package externalaccount
16
17 import (
18 "bytes"
19 "context"
20 "encoding/json"
21 "errors"
22 "fmt"
23 "net/http"
24 "os"
25 "os/exec"
26 "regexp"
27 "strings"
28 "time"
29
30 "cloud.google.com/go/auth/internal"
31 )
32
33 const (
34 executableSupportedMaxVersion = 1
35 executableDefaultTimeout = 30 * time.Second
36 executableSource = "response"
37 executableProviderType = "executable"
38 outputFileSource = "output file"
39
40 allowExecutablesEnvVar = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
41
42 jwtTokenType = "urn:ietf:params:oauth:token-type:jwt"
43 idTokenType = "urn:ietf:params:oauth:token-type:id_token"
44 saml2TokenType = "urn:ietf:params:oauth:token-type:saml2"
45 )
46
47 var (
48 serviceAccountImpersonationRE = regexp.MustCompile(`https://iamcredentials..+/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken`)
49 )
50
51 type nonCacheableError struct {
52 message string
53 }
54
55 func (nce nonCacheableError) Error() string {
56 return nce.message
57 }
58
59
60 type environment interface {
61 existingEnv() []string
62 getenv(string) string
63 run(ctx context.Context, command string, env []string) ([]byte, error)
64 now() time.Time
65 }
66
67 type runtimeEnvironment struct{}
68
69 func (r runtimeEnvironment) existingEnv() []string {
70 return os.Environ()
71 }
72 func (r runtimeEnvironment) getenv(key string) string {
73 return os.Getenv(key)
74 }
75 func (r runtimeEnvironment) now() time.Time {
76 return time.Now().UTC()
77 }
78
79 func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
80 splitCommand := strings.Fields(command)
81 cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
82 cmd.Env = env
83
84 var stdout, stderr bytes.Buffer
85 cmd.Stdout = &stdout
86 cmd.Stderr = &stderr
87
88 if err := cmd.Run(); err != nil {
89 if ctx.Err() == context.DeadlineExceeded {
90 return nil, context.DeadlineExceeded
91 }
92 if exitError, ok := err.(*exec.ExitError); ok {
93 return nil, exitCodeError(exitError)
94 }
95 return nil, executableError(err)
96 }
97
98 bytesStdout := bytes.TrimSpace(stdout.Bytes())
99 if len(bytesStdout) > 0 {
100 return bytesStdout, nil
101 }
102 return bytes.TrimSpace(stderr.Bytes()), nil
103 }
104
105 type executableSubjectProvider struct {
106 Command string
107 Timeout time.Duration
108 OutputFile string
109 client *http.Client
110 opts *Options
111 env environment
112 }
113
114 type executableResponse struct {
115 Version int `json:"version,omitempty"`
116 Success *bool `json:"success,omitempty"`
117 TokenType string `json:"token_type,omitempty"`
118 ExpirationTime int64 `json:"expiration_time,omitempty"`
119 IDToken string `json:"id_token,omitempty"`
120 SamlResponse string `json:"saml_response,omitempty"`
121 Code string `json:"code,omitempty"`
122 Message string `json:"message,omitempty"`
123 }
124
125 func (sp *executableSubjectProvider) parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
126 var result executableResponse
127 if err := json.Unmarshal(response, &result); err != nil {
128 return "", jsonParsingError(source, string(response))
129 }
130
131 if result.Version == 0 {
132 return "", missingFieldError(source, "version")
133 }
134 if result.Success == nil {
135 return "", missingFieldError(source, "success")
136 }
137 if !*result.Success {
138 if result.Code == "" || result.Message == "" {
139 return "", malformedFailureError()
140 }
141 return "", userDefinedError(result.Code, result.Message)
142 }
143 if result.Version > executableSupportedMaxVersion || result.Version < 0 {
144 return "", unsupportedVersionError(source, result.Version)
145 }
146 if result.ExpirationTime == 0 && sp.OutputFile != "" {
147 return "", missingFieldError(source, "expiration_time")
148 }
149 if result.TokenType == "" {
150 return "", missingFieldError(source, "token_type")
151 }
152 if result.ExpirationTime != 0 && result.ExpirationTime < now {
153 return "", tokenExpiredError()
154 }
155
156 switch result.TokenType {
157 case jwtTokenType, idTokenType:
158 if result.IDToken == "" {
159 return "", missingFieldError(source, "id_token")
160 }
161 return result.IDToken, nil
162 case saml2TokenType:
163 if result.SamlResponse == "" {
164 return "", missingFieldError(source, "saml_response")
165 }
166 return result.SamlResponse, nil
167 default:
168 return "", tokenTypeError(source)
169 }
170 }
171
172 func (sp *executableSubjectProvider) subjectToken(ctx context.Context) (string, error) {
173 if token, err := sp.getTokenFromOutputFile(); token != "" || err != nil {
174 return token, err
175 }
176 return sp.getTokenFromExecutableCommand(ctx)
177 }
178
179 func (sp *executableSubjectProvider) providerType() string {
180 return executableProviderType
181 }
182
183 func (sp *executableSubjectProvider) getTokenFromOutputFile() (token string, err error) {
184 if sp.OutputFile == "" {
185
186 return "", nil
187 }
188
189 file, err := os.Open(sp.OutputFile)
190 if err != nil {
191
192 return "", nil
193 }
194 defer file.Close()
195
196 data, err := internal.ReadAll(file)
197 if err != nil || len(data) == 0 {
198
199 return "", nil
200 }
201
202 token, err = sp.parseSubjectTokenFromSource(data, outputFileSource, sp.env.now().Unix())
203 if err != nil {
204 if _, ok := err.(nonCacheableError); ok {
205
206
207 return "", nil
208 }
209
210
211 return "", err
212 }
213
214 return token, nil
215 }
216
217 func (sp *executableSubjectProvider) executableEnvironment() []string {
218 result := sp.env.existingEnv()
219 result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", sp.opts.Audience))
220 result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", sp.opts.SubjectTokenType))
221 result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
222 if sp.opts.ServiceAccountImpersonationURL != "" {
223 matches := serviceAccountImpersonationRE.FindStringSubmatch(sp.opts.ServiceAccountImpersonationURL)
224 if matches != nil {
225 result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
226 }
227 }
228 if sp.OutputFile != "" {
229 result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", sp.OutputFile))
230 }
231 return result
232 }
233
234 func (sp *executableSubjectProvider) getTokenFromExecutableCommand(ctx context.Context) (string, error) {
235
236 if sp.env.getenv(allowExecutablesEnvVar) != "1" {
237 return "", errors.New("credentials: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
238 }
239
240 ctx, cancel := context.WithDeadline(ctx, sp.env.now().Add(sp.Timeout))
241 defer cancel()
242
243 output, err := sp.env.run(ctx, sp.Command, sp.executableEnvironment())
244 if err != nil {
245 return "", err
246 }
247 return sp.parseSubjectTokenFromSource(output, executableSource, sp.env.now().Unix())
248 }
249
250 func missingFieldError(source, field string) error {
251 return fmt.Errorf("credentials: %q missing %q field", source, field)
252 }
253
254 func jsonParsingError(source, data string) error {
255 return fmt.Errorf("credentials: unable to parse %q: %v", source, data)
256 }
257
258 func malformedFailureError() error {
259 return nonCacheableError{"credentials: response must include `error` and `message` fields when unsuccessful"}
260 }
261
262 func userDefinedError(code, message string) error {
263 return nonCacheableError{fmt.Sprintf("credentials: response contains unsuccessful response: (%v) %v", code, message)}
264 }
265
266 func unsupportedVersionError(source string, version int) error {
267 return fmt.Errorf("credentials: %v contains unsupported version: %v", source, version)
268 }
269
270 func tokenExpiredError() error {
271 return nonCacheableError{"credentials: the token returned by the executable is expired"}
272 }
273
274 func tokenTypeError(source string) error {
275 return fmt.Errorf("credentials: %v contains unsupported token type", source)
276 }
277
278 func exitCodeError(err *exec.ExitError) error {
279 return fmt.Errorf("credentials: executable command failed with exit code %v: %w", err.ExitCode(), err)
280 }
281
282 func executableError(err error) error {
283 return fmt.Errorf("credentials: executable command failed: %w", err)
284 }
285
View as plain text