1 package cli
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import (
18 "bytes"
19 "encoding/json"
20 "fmt"
21 "os"
22 "os/exec"
23 "path/filepath"
24 "regexp"
25 "runtime"
26 "strconv"
27 "time"
28
29 "github.com/Azure/go-autorest/autorest/adal"
30 "github.com/Azure/go-autorest/autorest/date"
31 "github.com/mitchellh/go-homedir"
32 )
33
34
35 type Token struct {
36 AccessToken string `json:"accessToken"`
37 Authority string `json:"_authority"`
38 ClientID string `json:"_clientId"`
39 ExpiresOn string `json:"expiresOn"`
40 IdentityProvider string `json:"identityProvider"`
41 IsMRRT bool `json:"isMRRT"`
42 RefreshToken string `json:"refreshToken"`
43 Resource string `json:"resource"`
44 TokenType string `json:"tokenType"`
45 UserID string `json:"userId"`
46 }
47
48 const accessTokensJSON = "accessTokens.json"
49
50
51 func (t Token) ToADALToken() (converted adal.Token, err error) {
52 tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn)
53 if err != nil {
54 err = fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err)
55 return
56 }
57
58 difference := tokenExpirationDate.Sub(date.UnixEpoch())
59
60 converted = adal.Token{
61 AccessToken: t.AccessToken,
62 Type: t.TokenType,
63 ExpiresIn: "3600",
64 ExpiresOn: json.Number(strconv.Itoa(int(difference.Seconds()))),
65 RefreshToken: t.RefreshToken,
66 Resource: t.Resource,
67 }
68 return
69 }
70
71
72
73 func AccessTokensPath() (string, error) {
74
75 if accessTokenPath := os.Getenv("AZURE_ACCESS_TOKEN_FILE"); accessTokenPath != "" {
76 return accessTokenPath, nil
77 }
78
79
80 if cfgDir := configDir(); cfgDir != "" {
81 return filepath.Join(cfgDir, accessTokensJSON), nil
82 }
83
84
85
86 return homedir.Expand("~/.azure/" + accessTokensJSON)
87 }
88
89
90 func ParseExpirationDate(input string) (*time.Time, error) {
91
92 expirationDate, cloudShellErr := time.Parse(time.RFC3339, input)
93 if cloudShellErr != nil {
94
95 const cliFormat = "2006-01-02 15:04:05.999999"
96 expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local)
97 if cliErr == nil {
98 return &expirationDate, nil
99 }
100
101 return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr)
102 }
103
104 return &expirationDate, nil
105 }
106
107
108 func LoadTokens(path string) ([]Token, error) {
109 file, err := os.Open(path)
110 if err != nil {
111 return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
112 }
113 defer file.Close()
114
115 var tokens []Token
116
117 dec := json.NewDecoder(file)
118 if err = dec.Decode(&tokens); err != nil {
119 return nil, fmt.Errorf("failed to decode contents of file (%s) into a `cli.Token` representation: %v", path, err)
120 }
121
122 return tokens, nil
123 }
124
125
126 func GetTokenFromCLI(resource string) (*Token, error) {
127 return GetTokenFromCLIWithParams(GetAccessTokenParams{Resource: resource})
128 }
129
130
131 type GetAccessTokenParams struct {
132 Resource string
133 ResourceType string
134 Subscription string
135 Tenant string
136 }
137
138
139 func GetTokenFromCLIWithParams(params GetAccessTokenParams) (*Token, error) {
140 cliCmd := GetAzureCLICommand()
141
142 cliCmd.Args = append(cliCmd.Args, "account", "get-access-token", "-o", "json")
143 if params.Resource != "" {
144 if err := validateParameter(params.Resource); err != nil {
145 return nil, err
146 }
147 cliCmd.Args = append(cliCmd.Args, "--resource", params.Resource)
148 }
149 if params.ResourceType != "" {
150 if err := validateParameter(params.ResourceType); err != nil {
151 return nil, err
152 }
153 cliCmd.Args = append(cliCmd.Args, "--resource-type", params.ResourceType)
154 }
155 if params.Subscription != "" {
156 if err := validateParameter(params.Subscription); err != nil {
157 return nil, err
158 }
159 cliCmd.Args = append(cliCmd.Args, "--subscription", params.Subscription)
160 }
161 if params.Tenant != "" {
162 if err := validateParameter(params.Tenant); err != nil {
163 return nil, err
164 }
165 cliCmd.Args = append(cliCmd.Args, "--tenant", params.Tenant)
166 }
167
168 var stderr bytes.Buffer
169 cliCmd.Stderr = &stderr
170
171 output, err := cliCmd.Output()
172 if err != nil {
173 if stderr.Len() > 0 {
174 return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", stderr.String())
175 }
176
177 return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", err.Error())
178 }
179
180 tokenResponse := Token{}
181 err = json.Unmarshal(output, &tokenResponse)
182 if err != nil {
183 return nil, err
184 }
185
186 return &tokenResponse, err
187 }
188
189 func validateParameter(param string) error {
190
191 const invalidResourceErrorTemplate = "Parameter %s is not in expected format. Only alphanumeric characters, [dot], [colon], [hyphen], and [forward slash] are allowed."
192 match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", param)
193 if err != nil {
194 return err
195 }
196 if !match {
197 return fmt.Errorf(invalidResourceErrorTemplate, param)
198 }
199 return nil
200 }
201
202
203 func GetAzureCLICommand() *exec.Cmd {
204
205 const azureCLIPath = "AzureCLIPath"
206
207
208 azureCLIDefaultPathWindows := fmt.Sprintf("%s\\Microsoft SDKs\\Azure\\CLI2\\wbin; %s\\Microsoft SDKs\\Azure\\CLI2\\wbin", os.Getenv("ProgramFiles(x86)"), os.Getenv("ProgramFiles"))
209
210
211 const azureCLIDefaultPath = "/bin:/sbin:/usr/bin:/usr/local/bin"
212
213
214 var cliCmd *exec.Cmd
215 if runtime.GOOS == "windows" {
216 cliCmd = exec.Command(fmt.Sprintf("%s\\system32\\cmd.exe", os.Getenv("windir")))
217 cliCmd.Env = os.Environ()
218 cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s;%s", os.Getenv(azureCLIPath), azureCLIDefaultPathWindows))
219 cliCmd.Args = append(cliCmd.Args, "/c", "az")
220 } else {
221 cliCmd = exec.Command("az")
222 cliCmd.Env = os.Environ()
223 cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s:%s", os.Getenv(azureCLIPath), azureCLIDefaultPath))
224 }
225
226 return cliCmd
227 }
228
View as plain text