developer_credential_util.go raw

   1  //go:build go1.18
   2  // +build go1.18
   3  
   4  // Copyright (c) Microsoft Corporation. All rights reserved.
   5  // Licensed under the MIT License.
   6  
   7  package azidentity
   8  
   9  import (
  10  	"bytes"
  11  	"context"
  12  	"errors"
  13  	"os"
  14  	"os/exec"
  15  	"strings"
  16  	"time"
  17  )
  18  
  19  // cliTimeout is the default timeout for authentication attempts via CLI tools
  20  const cliTimeout = 10 * time.Second
  21  
  22  // executor runs a command and returns its output or an error
  23  type executor func(ctx context.Context, credName, command string) ([]byte, error)
  24  
  25  var shellExec = func(ctx context.Context, credName, command string) ([]byte, error) {
  26  	// set a default timeout for this authentication iff the caller hasn't done so already
  27  	var cancel context.CancelFunc
  28  	if _, hasDeadline := ctx.Deadline(); !hasDeadline {
  29  		ctx, cancel = context.WithTimeout(ctx, cliTimeout)
  30  		defer cancel()
  31  	}
  32  	cmd, err := buildCmd(ctx, credName, command)
  33  	if err != nil {
  34  		return nil, err
  35  	}
  36  	cmd.Env = os.Environ()
  37  	stderr := bytes.Buffer{}
  38  	cmd.Stderr = &stderr
  39  	cmd.WaitDelay = 100 * time.Millisecond
  40  
  41  	stdout, err := cmd.Output()
  42  	if errors.Is(err, exec.ErrWaitDelay) && len(stdout) > 0 {
  43  		// The child process wrote to stdout and exited without closing it.
  44  		// Swallow this error and return stdout because it may contain a token.
  45  		return stdout, nil
  46  	}
  47  	if err != nil {
  48  		msg := stderr.String()
  49  		var exErr *exec.ExitError
  50  		if errors.As(err, &exErr) && exErr.ExitCode() == 127 || strings.Contains(msg, "' is not recognized") {
  51  			return nil, newCredentialUnavailableError(credName, "executable not found on path")
  52  		}
  53  		if credName == credNameAzurePowerShell {
  54  			if strings.Contains(msg, "Connect-AzAccount") {
  55  				msg = `Please run "Connect-AzAccount" to set up an account`
  56  			}
  57  			if strings.Contains(msg, noAzAccountModule) {
  58  				msg = noAzAccountModule
  59  			}
  60  		}
  61  		if msg == "" {
  62  			msg = err.Error()
  63  		}
  64  		return nil, newAuthenticationFailedError(credName, msg, nil)
  65  	}
  66  
  67  	return stdout, nil
  68  }
  69  
  70  // unavailableIfInDAC returns err or, if the credential was invoked by DefaultAzureCredential, a
  71  // credentialUnavailableError having the same message. This ensures DefaultAzureCredential will try
  72  // the next credential in its chain (another developer credential).
  73  func unavailableIfInDAC(err error, inDefaultChain bool) error {
  74  	if err != nil && inDefaultChain && !errors.As(err, new(credentialUnavailable)) {
  75  		err = NewCredentialUnavailableError(err.Error())
  76  	}
  77  	return err
  78  }
  79  
  80  // validScope is for credentials authenticating via external tools. The authority validates scopes for all other credentials.
  81  func validScope(scope string) bool {
  82  	for _, r := range scope {
  83  		if !(alphanumeric(r) || r == '.' || r == '-' || r == '_' || r == '/' || r == ':') {
  84  			return false
  85  		}
  86  	}
  87  	return true
  88  }
  89