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