forked from Mirrors/oauth2
Merge pull request #4 from ScruffyProdigy/timeout
Use correct detection method for timeout errors
This commit is contained in:
commit
98f37871ca
|
@ -163,7 +163,7 @@ type format struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
||||||
// Either the File or the URL field should be filled, depending on the kind of credential in question.
|
// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
|
||||||
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
||||||
type CredentialSource struct {
|
type CredentialSource struct {
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
package externalaccount
|
package externalaccount
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -18,7 +18,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var serviceAccountImpersonationCompiler = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
|
var serviceAccountImpersonationRE = regexp.MustCompile("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/(.*@.*):generateAccessToken")
|
||||||
|
|
||||||
const (
|
const (
|
||||||
executableSupportedMaxVersion = 1
|
executableSupportedMaxVersion = 1
|
||||||
|
@ -38,7 +38,7 @@ func (nce nonCacheableError) Error() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func missingFieldError(source, field string) error {
|
func missingFieldError(source, field string) error {
|
||||||
return fmt.Errorf("oauth2/google: %v missing `%v` field", source, field)
|
return fmt.Errorf("oauth2/google: %v missing `%q` field", source, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonParsingError(source, data string) error {
|
func jsonParsingError(source, data string) error {
|
||||||
|
@ -65,10 +65,6 @@ func tokenTypeError(source string) error {
|
||||||
return fmt.Errorf("oauth2/google: %v contains unsupported token type", source)
|
return fmt.Errorf("oauth2/google: %v contains unsupported token type", source)
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeoutError() error {
|
|
||||||
return errors.New("oauth2/google: executable command timed out")
|
|
||||||
}
|
|
||||||
|
|
||||||
func exitCodeError(exitCode int) error {
|
func exitCodeError(exitCode int) error {
|
||||||
return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode)
|
return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode)
|
||||||
}
|
}
|
||||||
|
@ -82,36 +78,60 @@ func executablesDisallowedError() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeoutRangeError() error {
|
func timeoutRangeError() error {
|
||||||
return errors.New("oauth2/google: invalid `timeout_millis` field. Executable timeout must be between 5 and 120 seconds")
|
return errors.New("oauth2/google: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandMissingError() error {
|
func commandMissingError() error {
|
||||||
return errors.New("oauth2/google: missing `command` field. Executable command must be provided")
|
return errors.New("oauth2/google: missing `command` field — executable command must be provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
// baseEnv is an alias of os.Environ used for testing
|
type environment interface {
|
||||||
var baseEnv = os.Environ
|
existingEnv() []string
|
||||||
|
getenv(string) string
|
||||||
|
run(ctx context.Context, command string, env []string) ([]byte, error)
|
||||||
|
now() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// runCommand is basically an alias of exec.CommandContext for testing.
|
type runtimeEnvironment struct{}
|
||||||
var runCommand = func(ctx context.Context, command string, env []string) ([]byte, error) {
|
|
||||||
|
func (r runtimeEnvironment) existingEnv() []string {
|
||||||
|
return os.Environ()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r runtimeEnvironment) getenv(key string) string {
|
||||||
|
return os.Getenv(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r runtimeEnvironment) now() time.Time {
|
||||||
|
return time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r runtimeEnvironment) run(ctx context.Context, command string, env []string) ([]byte, error) {
|
||||||
splitCommand := strings.Fields(command)
|
splitCommand := strings.Fields(command)
|
||||||
cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
|
cmd := exec.CommandContext(ctx, splitCommand[0], splitCommand[1:]...)
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
|
|
||||||
response, err := cmd.Output()
|
var stdout, stderr bytes.Buffer
|
||||||
if err == nil {
|
cmd.Stdout = &stdout
|
||||||
return response, nil
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return nil, context.DeadlineExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
return nil, exitCodeError(exitError.ExitCode())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, executableError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == context.DeadlineExceeded {
|
bytesStdout := bytes.TrimSpace(stdout.Bytes())
|
||||||
return nil, timeoutError()
|
if len(bytesStdout) > 0 {
|
||||||
|
return bytesStdout, nil
|
||||||
}
|
}
|
||||||
|
return bytes.TrimSpace(stderr.Bytes()), nil
|
||||||
if exitError, ok := err.(*exec.ExitError); ok {
|
|
||||||
return nil, exitCodeError(exitError.ExitCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, executableError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type executableCredentialSource struct {
|
type executableCredentialSource struct {
|
||||||
|
@ -120,28 +140,31 @@ type executableCredentialSource struct {
|
||||||
OutputFile string
|
OutputFile string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
config *Config
|
config *Config
|
||||||
|
env environment
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
||||||
// It also performs defaulting and type conversions.
|
// It also performs defaulting and type conversions.
|
||||||
func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (result executableCredentialSource, err error) {
|
func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
|
||||||
if ec.Command == "" {
|
if ec.Command == "" {
|
||||||
err = commandMissingError()
|
return executableCredentialSource{}, commandMissingError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := executableCredentialSource{}
|
||||||
result.Command = ec.Command
|
result.Command = ec.Command
|
||||||
if ec.TimeoutMillis == nil {
|
if ec.TimeoutMillis == nil {
|
||||||
result.Timeout = defaultTimeout
|
result.Timeout = defaultTimeout
|
||||||
} else {
|
} else {
|
||||||
result.Timeout = time.Duration(*ec.TimeoutMillis) * time.Millisecond
|
result.Timeout = time.Duration(*ec.TimeoutMillis) * time.Millisecond
|
||||||
if result.Timeout < timeoutMinimum || result.Timeout > timeoutMaximum {
|
if result.Timeout < timeoutMinimum || result.Timeout > timeoutMaximum {
|
||||||
err = timeoutRangeError()
|
return executableCredentialSource{}, timeoutRangeError()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.OutputFile = ec.OutputFile
|
result.OutputFile = ec.OutputFile
|
||||||
result.ctx = ctx
|
result.ctx = ctx
|
||||||
result.config = config
|
result.config = config
|
||||||
return
|
result.env = runtimeEnvironment{}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type executableResponse struct {
|
type executableResponse struct {
|
||||||
|
@ -155,7 +178,7 @@ type executableResponse struct {
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSubjectTokenFromSource(response []byte, source string) (string, error) {
|
func parseSubjectTokenFromSource(response []byte, source string, now int64) (string, error) {
|
||||||
var result executableResponse
|
var result executableResponse
|
||||||
if err := json.Unmarshal(response, &result); err != nil {
|
if err := json.Unmarshal(response, &result); err != nil {
|
||||||
return "", jsonParsingError(source, string(response))
|
return "", jsonParsingError(source, string(response))
|
||||||
|
@ -188,7 +211,7 @@ func parseSubjectTokenFromSource(response []byte, source string) (string, error)
|
||||||
return "", missingFieldError(source, "token_type")
|
return "", missingFieldError(source, "token_type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.ExpirationTime < now().Unix() {
|
if result.ExpirationTime < now {
|
||||||
return "", tokenExpiredError()
|
return "", tokenExpiredError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,88 +233,76 @@ func parseSubjectTokenFromSource(response []byte, source string) (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs executableCredentialSource) subjectToken() (string, error) {
|
func (cs executableCredentialSource) subjectToken() (string, error) {
|
||||||
if token, err, ok := cs.getTokenFromOutputFile(); ok {
|
if token, err := cs.getTokenFromOutputFile(); token != "" || err != nil {
|
||||||
return token, err
|
return token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cs.getTokenFromExecutableCommand()
|
return cs.getTokenFromExecutableCommand()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs executableCredentialSource) getTokenFromOutputFile() (string, error, bool) {
|
func (cs executableCredentialSource) getTokenFromOutputFile() (token string, err error) {
|
||||||
if cs.OutputFile == "" {
|
if cs.OutputFile == "" {
|
||||||
// This ExecutableCredentialSource doesn't use an OutputFile.
|
// This ExecutableCredentialSource doesn't use an OutputFile.
|
||||||
return "", nil, false
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
file, err := os.Open(cs.OutputFile)
|
file, err := os.Open(cs.OutputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No OutputFile found. Hasn't been created yet, so skip it.
|
// No OutputFile found. Hasn't been created yet, so skip it.
|
||||||
return "", nil, false
|
return "", nil
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(io.LimitReader(file, 1<<20))
|
data, err := io.ReadAll(io.LimitReader(file, 1<<20))
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil || len(data) == 0 {
|
||||||
// Cachefile exists, but no data found. Get new credential.
|
// Cachefile exists, but no data found. Get new credential.
|
||||||
return "", nil, false
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := parseSubjectTokenFromSource(data, outputFileSource)
|
token, err = parseSubjectTokenFromSource(data, outputFileSource, cs.env.now().Unix())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(nonCacheableError); ok {
|
if _, ok := err.(nonCacheableError); ok {
|
||||||
// If the cached token is expired we need a new token,
|
// If the cached token is expired we need a new token,
|
||||||
// and if the cache contains a failure, we need to try again.
|
// and if the cache contains a failure, we need to try again.
|
||||||
return "", nil, false
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// There was an error in the cached token, and the developer should be aware of it.
|
// There was an error in the cached token, and the developer should be aware of it.
|
||||||
return "", err, true
|
return "", err
|
||||||
}
|
}
|
||||||
// Token parsing succeeded. Use found token.
|
// Token parsing succeeded. Use found token.
|
||||||
return token, nil, true
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs executableCredentialSource) getEnvironment() []string {
|
func (cs executableCredentialSource) executableEnvironment() []string {
|
||||||
result := baseEnv()
|
result := cs.env.existingEnv()
|
||||||
for k, v := range cs.getNewEnvironmentVariables() {
|
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE=%v", cs.config.Audience))
|
||||||
result = append(result, fmt.Sprintf("%v=%v", k, v))
|
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE=%v", cs.config.SubjectTokenType))
|
||||||
}
|
result = append(result, "GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE=0")
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs executableCredentialSource) getNewEnvironmentVariables() map[string]string {
|
|
||||||
result := map[string]string{
|
|
||||||
"GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE": cs.config.Audience,
|
|
||||||
"GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE": cs.config.SubjectTokenType,
|
|
||||||
"GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE": "0",
|
|
||||||
}
|
|
||||||
|
|
||||||
if cs.config.ServiceAccountImpersonationURL != "" {
|
if cs.config.ServiceAccountImpersonationURL != "" {
|
||||||
matches := serviceAccountImpersonationCompiler.FindStringSubmatch(cs.config.ServiceAccountImpersonationURL)
|
matches := serviceAccountImpersonationRE.FindStringSubmatch(cs.config.ServiceAccountImpersonationURL)
|
||||||
if matches != nil {
|
if matches != nil {
|
||||||
result["GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"] = matches[1]
|
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL=%v", matches[1]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cs.OutputFile != "" {
|
if cs.OutputFile != "" {
|
||||||
result["GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"] = cs.OutputFile
|
result = append(result, fmt.Sprintf("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE=%v", cs.OutputFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, error) {
|
func (cs executableCredentialSource) getTokenFromExecutableCommand() (string, error) {
|
||||||
// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
|
// For security reasons, we need our consumers to set this environment variable to allow executables to be run.
|
||||||
if getenv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") != "1" {
|
if cs.env.getenv("GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES") != "1" {
|
||||||
return "", executablesDisallowedError()
|
return "", executablesDisallowedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithDeadline(cs.ctx, now().Add(cs.Timeout))
|
ctx, cancel := context.WithDeadline(cs.ctx, cs.env.now().Add(cs.Timeout))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if output, err := runCommand(ctx, cs.Command, cs.getEnvironment()); err != nil {
|
output, err := cs.env.run(ctx, cs.Command, cs.executableEnvironment())
|
||||||
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
} else {
|
|
||||||
return parseSubjectTokenFromSource(output, executableSource)
|
|
||||||
}
|
}
|
||||||
|
return parseSubjectTokenFromSource(output, executableSource, cs.env.now().Unix())
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue