/
cloudshell.go
393 lines (344 loc) · 10.7 KB
/
cloudshell.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
package azure
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"path"
"strings"
"time"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/cli"
"github.com/mitchellh/go-homedir"
log "github.com/sirupsen/logrus"
"gopkg.in/go-ini/ini.v1"
)
const (
azureCLIConfigFileName = "config"
)
type checkAccessResponse []accessResponse
type accessResponse struct {
AccessDecision string `json:"accessDecision"`
}
type checkAccessRequest struct {
Subject subject `json:"Subject"`
Actions []action `json:"Actions"`
}
type action struct {
ID string `json:"Id"`
}
type subject struct {
Attributes attributes `json:"Attributes"`
}
type attributes struct {
ObjectID string `json:"ObjectId"`
}
type cloudrive struct {
StorageAccountResourceID string `json:"storageAccountResourceId"`
FileShareName string `json:"fileShareName"`
Size int `json:"diskSizeInGB"`
}
// FileShareDetails contains details of the clouddrive FileShare
type FileShareDetails struct {
Name string
StorageAccountName string
StorageAccountKey string
}
// IsInCloudShell checks if we are currently running in CloudShell
func IsInCloudShell() bool {
return len(os.Getenv("ACC_CLOUD")) > 0
}
// GetCloudShellLocation returns the location that CloudShell is running in
func GetCloudShellLocation() string {
if !IsInCloudShell() {
return ""
}
return os.Getenv("ACC_LOCATION")
}
func getSubscriptionsfromCLIProfile() *[]cli.Subscription {
subscriptions := []cli.Subscription{}
if !IsInCloudShell() {
return &subscriptions
}
// Do not throw errors as just return an empty struct
profilePath, err := cli.ProfilePath()
if err != nil {
log.Debug("Failed to get cli ProfilePath: ", err)
return &subscriptions
}
profile, err := cli.LoadProfile(profilePath)
if err != nil {
log.Debug("Failed to get cli Profile: ", err)
return &subscriptions
}
return &profile.Subscriptions
}
// GetTenantIDFromCliProfile gets tenant id from az cli profile
func GetTenantIDFromCliProfile() string {
if !IsInCloudShell() {
return ""
}
subscriptions := getSubscriptionsfromCLIProfile()
for _, subscription := range *subscriptions {
if strings.ToLower(subscription.EnvironmentName) == "azurecloud" && subscription.IsDefault {
return subscription.TenantID
}
}
return ""
}
// GetSubscriptionIDFromCliProfile gets default subscription id from az cli profile
func GetSubscriptionIDFromCliProfile() string {
if !IsInCloudShell() {
return ""
}
subscriptions := getSubscriptionsfromCLIProfile()
for _, subscription := range *subscriptions {
if strings.ToLower(subscription.EnvironmentName) == "azurecloud" && subscription.IsDefault {
log.Debug("Subscription from cli profile is: ", subscription.ID)
return subscription.ID
}
}
return ""
}
// TryGetRGandLocation tries to get Resource Group and Location Information from az defaults and ACC_env var
func TryGetRGandLocation() (rg string, location string) {
if !IsInCloudShell() {
return
}
// Get values from ENV , if not set try the config ini
rg, location = getRGAndLocationFromEnv()
if len(rg) == 0 || len(location) == 0 {
crg, clocation := getRGAndLocationFromConfig()
if len(rg) == 0 {
rg = crg
}
if len(location) == 0 {
location = clocation
}
}
// If neither value is set get the location of cloudshell
if len(location) == 0 && len(rg) == 0 {
location = os.Getenv("ACC_LOCATION")
}
log.Debug("Resource Group from CloudShell: ", rg)
location = strings.ToLower(strings.Replace(location, " ", "", -1))
log.Debug("Location from CloudShell: ", location)
return
}
func getRGAndLocationFromEnv() (rg string, location string) {
rg = os.Getenv("AZURE_DEFAULTS_GROUP")
location = os.Getenv("AZURE_DEFAULTS_LOCATION")
return
}
func getRGAndLocationFromConfig() (rg string, location string) {
path, err := getCLIConfig()
// Log errors and return empty string if cannot read from the az config
if err != nil {
log.Debug("failed to get cli config path:", err)
return
}
if _, err := os.Stat(path); err != nil {
log.Debug("failed to get cli config:", err)
return
}
cfg, err := ini.Load(path)
if err != nil {
log.Debug("failed to parse config ", err)
return
}
rg = cfg.Section("defaults").Key("group").String()
location = cfg.Section("defaults").Key("location").String()
return
}
// GetCloudDriveDetails gets the details of the clouddrive cloudshare
func GetCloudDriveDetails(userAgent string) (*FileShareDetails, error) {
if !IsInCloudShell() {
return nil, errors.New("Not Running in CloudShell")
}
clouddriveConfig, err := getCloudDriveConfig()
if err != nil {
return nil, err
}
resource := parseResourceID(clouddriveConfig.StorageAccountResourceID)
if resource == nil {
return nil, errors.New("Failed to Parse resource Id")
}
token, err := GetCloudShellToken()
if err != nil {
return nil, fmt.Errorf("failed to get CloudShell Token: %s", err)
}
authorizer := autorest.NewBearerAuthorizer(token)
client, err := GetStorageAccountsClient(resource.SubscriptionID, authorizer, userAgent)
if err != nil {
return nil, fmt.Errorf("Error getting Storage Accounts Client: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result, err := client.ListKeys(ctx, resource.ResourceGroup, resource.ResourceName, "")
if err != nil {
return nil, fmt.Errorf("failed to get strage account keys: %s", err)
}
return &FileShareDetails{
Name: clouddriveConfig.FileShareName,
StorageAccountName: resource.ResourceName,
StorageAccountKey: *(((*result.Keys)[0]).Value),
}, nil
}
// GetCloudDriveResourceGroup gets the resource group name associated with the clouddrive used in CloudShell
func GetCloudDriveResourceGroup() string {
if !IsInCloudShell() {
return ""
}
// return empty string if there are any errors
clouddriveConfig, err := getCloudDriveConfig()
if err != nil {
log.Debug("Error getting clouddrive config: ", err)
return ""
}
resource, err := azure.ParseResourceID(clouddriveConfig.StorageAccountResourceID)
if err != nil {
return ""
}
return resource.ResourceGroup
}
func parseResourceID(resourceID string) *azure.Resource {
resource, err := azure.ParseResourceID(resourceID)
if err != nil {
log.Debug("failed to parse StorageAccountResourceID: ", err)
}
return &resource
}
func getCloudDriveConfig() (*cloudrive, error) {
clouddriveConfig := cloudrive{}
if err := json.Unmarshal([]byte(os.Getenv("ACC_STORAGE_PROFILE")), &clouddriveConfig); err != nil {
return &clouddriveConfig, fmt.Errorf("failed to unmarshall ACC_STORAGE_PROFILE: %v ", err)
}
return &clouddriveConfig, nil
}
func getCLIConfig() (string, error) {
if cfgDir := os.Getenv("AZURE_CONFIG_DIR"); cfgDir != "" {
return path.Join(cfgDir, azureCLIConfigFileName), nil
}
return homedir.Expand("~/.azure/" + azureCLIConfigFileName)
}
// CheckCanAccessResource checks to see if the user can create a specific
func CheckCanAccessResource(actionID string, scope string) (bool, error) {
if !IsInCloudShell() {
return false, errors.New("Not Running in CloudShell")
}
adalToken, err := GetCloudShellToken()
if err != nil {
return false, fmt.Errorf("Error Getting CloudShellToken: %v", err)
}
oid, err := getFromToken(adalToken.AccessToken, "oid")
if err != nil {
return false, fmt.Errorf("failed to get Oid: %v ", err)
}
accessCheck := checkAccessRequest{
Actions: []action{
{
ID: actionID,
},
},
Subject: subject{
Attributes: attributes{
ObjectID: oid,
},
},
}
payload, err := json.Marshal(accessCheck)
if err != nil {
return false, fmt.Errorf("failed to serialise checkaccess payload: %v ", err)
}
log.Debug("Check Access POST Body ", string(payload))
return makeCheckAccessRequest(payload, scope)
}
func getFromToken(accessToken string, parameter string) (string, error) {
bearerToken := strings.Split(accessToken, ".")[1]
if len(bearerToken) == 0 {
return "", errors.New("Failed to get bearer token from CloudShell Token")
}
token, err := base64.RawStdEncoding.DecodeString(bearerToken)
if err != nil {
return "", fmt.Errorf("Failed to decode Bearer Token: %v ", err)
}
var adToken map[string]interface{}
if err := json.Unmarshal(token, &adToken); err != nil {
return "", fmt.Errorf("Failed to unmarshall CloudShell token: %v ", err)
}
parameterValue, hasParameter := adToken[parameter]
if hasParameter == false {
return "", errors.New("Requested token parameter not present")
}
return parameterValue.(string), err
}
func makeCheckAccessRequest(payload []byte, scope string) (bool, error) {
var err error
var response []byte
adalToken, err := GetCloudShellToken()
if err != nil {
return false, fmt.Errorf("Error Getting CloudShellToken: %v", err)
}
audUrl, err := getFromToken(adalToken.AccessToken, "aud")
if err != nil {
audUrl = "https://management.azure.com/"
}
retry:
for i := 1; i < 4; i++ {
timeout := time.Duration(time.Duration(i) * time.Second)
client := http.Client{
Timeout: timeout,
}
url := fmt.Sprintf("%s%s/providers/Microsoft.Authorization/CheckAccess", audUrl, scope)
log.Debug("Check Access URL: ", url)
var req *http.Request
req, err = http.NewRequest("POST", url, bytes.NewBuffer(payload))
if err != nil {
break retry
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", adalToken.AccessToken))
req.Header.Set("content-type", "application/json")
query := req.URL.Query()
query.Add("api-version", "2018-09-01-preview")
req.URL.RawQuery = query.Encode()
var resp *http.Response
resp, err = client.Do(req)
if err != nil {
break retry
}
var rawResp []byte
rawResp, err = ioutil.ReadAll(resp.Body)
if err != nil {
break retry
}
defer resp.Body.Close()
response = rawResp
log.Debug("Check Access HTTP Status Code: ", resp.StatusCode)
switch resp.StatusCode {
case http.StatusOK:
break retry
case http.StatusForbidden:
// User does not have permission to call CheckAccess so just return true, the operation may still succeed
return true, nil
default:
log.Debug(fmt.Sprintf("Error checking access HTTP Status: '%d'. Response Body: %s", resp.StatusCode, string(rawResp)))
err = fmt.Errorf("Error checking access HTTP Status: '%d'. Response Body: %s", resp.StatusCode, string(rawResp))
}
}
if err != nil {
return false, fmt.Errorf("Error Checking Access: %v", err)
}
log.Debug(fmt.Sprintf("Check Access Response Body: %s", string(response)))
var checkaccessresponse checkAccessResponse
err = json.Unmarshal(response, &checkaccessresponse)
if err != nil {
return false, fmt.Errorf("Error Unmarshalling Access Response: %v", err)
}
return strings.ToLower(checkaccessresponse[0].AccessDecision) == "allowed", nil
}