-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathauth_method_azure_cli_token.go
More file actions
371 lines (309 loc) · 11.8 KB
/
auth_method_azure_cli_token.go
File metadata and controls
371 lines (309 loc) · 11.8 KB
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
package authentication
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os/exec"
"strings"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure/cli"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-version"
"github.com/manicminer/hamilton/environments"
)
type azureCLIProfile struct {
// CLI "subscriptions" are really "accounts" that can represent either a subscription (with tenant) or _just_ a tenant
account *cli.Subscription
clientId string
environment string
subscriptionId string
tenantId string
tenantOnly bool
azVersion version.Version
}
type azureCliTokenAuth struct {
profile *azureCLIProfile
servicePrincipalAuthDocsLink string
}
func (a azureCliTokenAuth) build(b Builder) (authMethod, error) {
ver, err := populateAzVersion(b.TenantOnly)
if err != nil {
return nil, err
}
auth := azureCliTokenAuth{
profile: &azureCLIProfile{
subscriptionId: b.SubscriptionID,
tenantId: b.TenantID,
tenantOnly: b.TenantOnly,
clientId: "04b07795-8ddb-461a-bbee-02f9e1bf7b46", // fixed first party client id for Az CLI
azVersion: *ver,
},
servicePrincipalAuthDocsLink: b.ClientSecretDocsLink,
}
var acc *cli.Subscription
if auth.profile.tenantOnly {
var err error
acc, err = obtainTenant(b.TenantID)
if err != nil {
return nil, fmt.Errorf("obtain tenant(%s) from Azure CLI: %+v", b.TenantID, err)
}
auth.profile.account = acc
} else {
var err error
acc, err = obtainSubscription(b.SubscriptionID)
if err != nil {
return nil, fmt.Errorf("obtain subscription(%s) from Azure CLI: %+v", b.SubscriptionID, err)
}
auth.profile.account = acc
}
// Authenticating as a Service Principal doesn't return all of the information we need for authentication purposes
// as such Service Principal authentication is supported using the specific auth method
if acc.User == nil || !strings.EqualFold(acc.User.Type, "user") {
return nil, fmt.Errorf(`Authenticating using the Azure CLI is only supported as a User (not a Service Principal).
To authenticate to Azure using a Service Principal, you can use the separate 'Authenticate using a Service Principal'
auth method - instructions for which can be found here: %s
Alternatively you can authenticate using the Azure CLI by using a User Account.`, auth.servicePrincipalAuthDocsLink)
}
// Populate fields
if !b.TenantOnly && auth.profile.subscriptionId == "" {
auth.profile.subscriptionId = acc.ID
}
if auth.profile.tenantId == "" {
auth.profile.tenantId = acc.TenantID
}
// always pull the environment from the Azure CLI, since the Access Token's associated with it
auth.profile.environment = normalizeEnvironmentName(acc.EnvironmentName)
return auth, nil
}
func (a azureCliTokenAuth) isApplicable(b Builder) bool {
return b.SupportsAzureCliToken
}
func (a azureCliTokenAuth) getADALToken(_ context.Context, _ autorest.Sender, oauthConfig *OAuthConfig, endpoint string) (autorest.Authorizer, error) {
if oauthConfig.OAuth == nil {
return nil, fmt.Errorf("getting Authorization Token for cli auth: an OAuth token wasn't configured correctly; please file a bug with more details")
}
// the Azure CLI appears to cache these, so to maintain compatibility with the interface this method is intentionally not on the pointer
var token *cli.Token
var err error
if a.profile.tenantOnly {
token, err = obtainAuthorizationToken(endpoint, "", a.profile.tenantId)
} else {
token, err = obtainAuthorizationToken(endpoint, a.profile.subscriptionId, "")
}
if err != nil {
return nil, fmt.Errorf("obtaining Authorization Token from the Azure CLI: %s", err)
}
adalToken, err := token.ToADALToken()
if err != nil {
return nil, fmt.Errorf("converting Authorization Token to an ADAL Token: %s", err)
}
spt, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig.OAuth, a.profile.clientId, endpoint, adalToken)
if err != nil {
return nil, err
}
var refreshFunc adal.TokenRefresh = func(ctx context.Context, resource string) (*adal.Token, error) {
var token *cli.Token
var err error
if a.profile.tenantOnly {
token, err = obtainAuthorizationToken(resource, "", a.profile.tenantId)
} else {
token, err = obtainAuthorizationToken(resource, a.profile.subscriptionId, "")
}
if err != nil {
return nil, err
}
adalToken, err := token.ToADALToken()
if err != nil {
return nil, err
}
return &adalToken, nil
}
spt.SetCustomRefreshFunc(refreshFunc)
auth := autorest.NewBearerAuthorizer(spt)
return auth, nil
}
func (a azureCliTokenAuth) getMSALToken(ctx context.Context, _ environments.Api, sender autorest.Sender, oauthConfig *OAuthConfig, endpoint string) (autorest.Authorizer, error) {
// token version is the decision of az-cli, so we'll pass through to the existing method for continuity
return a.getADALToken(ctx, sender, oauthConfig, endpoint)
}
func (a azureCliTokenAuth) name() string {
return "Obtaining a token from the Azure CLI"
}
func (a azureCliTokenAuth) populateConfig(c *Config) error {
c.ClientID = a.profile.clientId
c.TenantID = a.profile.tenantId
c.Environment = a.profile.environment
c.SubscriptionID = a.profile.subscriptionId
c.GetAuthenticatedObjectID = func(ctx context.Context) (*string, error) {
objectId, err := obtainAuthenticatedObjectID(a.profile.azVersion)
if err != nil {
return nil, err
}
return objectId, nil
}
return nil
}
func (a azureCliTokenAuth) validate() error {
var err *multierror.Error
errorMessageFmt := "A %s was not found in your Azure CLI Credentials.\n\nPlease login to the Azure CLI again via `az login`"
if a.profile == nil {
return fmt.Errorf("Azure CLI Profile is nil - this is an internal error and should be reported.")
}
if a.profile.clientId == "" {
err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Client ID"))
}
if !a.profile.tenantOnly && a.profile.subscriptionId == "" {
err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Subscription ID"))
}
if a.profile.tenantId == "" {
err = multierror.Append(err, fmt.Errorf(errorMessageFmt, "Tenant ID"))
}
return err.ErrorOrNil()
}
func obtainAuthenticatedObjectID(azVersion version.Version) (*string, error) {
// Since v2.37.0, CLI migrated the underlying API for `az ad` to using MS graph: https://github.com/Azure/azure-cli/pull/22432.
// This causes different format for the output of `az ad signed-in-user show -o=json`.
v2_37 := version.Must(version.NewVersion("2.37.0"))
args := []string{"ad", "signed-in-user", "show", "-o=json"}
if azVersion.LessThan(v2_37) {
var json struct {
ObjectId string `json:"objectId"`
}
if err := jsonUnmarshalAzCmd(&json, args...); err != nil {
return nil, fmt.Errorf("parsing json result from the Azure CLI: %v", err)
}
return &json.ObjectId, nil
}
var json struct {
Id string `json:"id"`
}
if err := jsonUnmarshalAzCmd(&json, args...); err != nil {
return nil, fmt.Errorf("parsing json result from the Azure CLI: %v", err)
}
return &json.Id, nil
}
func populateAzVersion(tenantOnly bool) (*version.Version, error) {
// Azure CLI v2.0.79 is the earliest version to have a `version` command
var minimumVersion string
if tenantOnly {
// v2.0.81 introduced the `--tenant` option to the `account get-access-token` subcommand
minimumVersion = "2.0.81"
} else {
minimumVersion = "2.0.79"
}
var cliVersion *struct {
AzureCli *string `json:"azure-cli,omitempty"`
AzureCliCore *string `json:"azure-cli-core,omitempty"`
AzureCliTelemetry *string `json:"azure-cli-telemetry,omitempty"`
Extensions *interface{} `json:"extensions,omitempty"`
}
err := jsonUnmarshalAzCmd(&cliVersion, "version", "-o=json")
if err != nil {
return nil, fmt.Errorf("please ensure you have installed Azure CLI version %s or newer. Error parsing json result from the Azure CLI: %v.", minimumVersion, err)
}
if cliVersion.AzureCli == nil {
return nil, fmt.Errorf("could not detect Azure CLI version. Please ensure you have installed Azure CLI version %s or newer.", minimumVersion)
}
actual, err := version.NewVersion(*cliVersion.AzureCli)
if err != nil {
return nil, fmt.Errorf("could not parse detected Azure CLI version %q: %+v", *cliVersion.AzureCli, err)
}
supported := version.Must(version.NewVersion(minimumVersion))
nextMajor := version.Must(version.NewVersion("3.0.0"))
if nextMajor.LessThanOrEqual(actual) {
return nil, fmt.Errorf(`Authenticating using the Azure CLI requires a version older than %[1]s but Terraform detected version %[3]s.
Please install v%[2]s or newer (but also older than %[1]s) and ensure the correct version is in your path.`, nextMajor.String(), supported.String(), actual.String())
}
if actual.LessThan(supported) {
return nil, fmt.Errorf(`Authenticating using the Azure CLI requires version %[1]s but Terraform detected version %[2]s.
Please install v%[1]s or greater and ensure the correct version is in your path.`, supported.String(), actual.String())
}
return actual, nil
}
func obtainAuthorizationToken(endpoint string, subscriptionId string, tenantId string) (*cli.Token, error) {
var token cli.Token
var err error
if tenantId != "" {
err = jsonUnmarshalAzCmd(&token, "account", "get-access-token", "--resource", endpoint, "--tenant", tenantId, "-o=json")
} else {
err = jsonUnmarshalAzCmd(&token, "account", "get-access-token", "--resource", endpoint, "--subscription", subscriptionId, "-o=json")
}
if err != nil {
return nil, fmt.Errorf("parsing json result from the Azure CLI: %v", err)
}
return &token, nil
}
// obtainSubscription returns a Subscription object of the specified subscriptionId.
// If the subscriptionId is empty, it selects the default subscription.
func obtainSubscription(subscriptionId string) (*cli.Subscription, error) {
var acc cli.Subscription
cmd := make([]string, 0)
cmd = []string{"account", "show", "-o=json"}
if subscriptionId != "" {
cmd = append(cmd, "-s", subscriptionId)
}
err := jsonUnmarshalAzCmd(&acc, cmd...)
if err != nil {
return nil, fmt.Errorf("parsing json result from the Azure CLI: %v", err)
}
return &acc, nil
}
// obtainTenant returns a Subscription object having the specified tenantId.
// If the tenantId is empty, it selects the default subscription.
// This works with `az login --allow-no-subscriptions`
func obtainTenant(tenantId string) (*cli.Subscription, error) {
var acc cli.Subscription
if tenantId == "" {
cmd := make([]string, 0)
cmd = []string{"account", "show", "-o=json"}
err := jsonUnmarshalAzCmd(&acc, cmd...)
if err != nil {
return nil, fmt.Errorf("parsing json result from the Azure CLI: %v", err)
}
} else {
var accs []cli.Subscription
cmd := make([]string, 0)
cmd = []string{"account", "list", "-o=json"}
err := jsonUnmarshalAzCmd(&accs, cmd...)
if err != nil {
return nil, fmt.Errorf("parsing json result from the Azure CLI: %v", err)
}
for _, a := range accs {
if a.TenantID == tenantId {
acc = a
break
}
}
if acc.TenantID == "" {
return nil, fmt.Errorf("tenant %q was not found", tenantId)
}
}
return &acc, nil
}
func jsonUnmarshalAzCmd(i interface{}, arg ...string) error {
var stderr bytes.Buffer
var stdout bytes.Buffer
cmd := exec.Command("az", arg...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Start(); err != nil {
err := fmt.Errorf("launching Azure CLI: %+v", err)
if stdErrStr := stderr.String(); stdErrStr != "" {
err = fmt.Errorf("%s: %s", err, strings.TrimSpace(stdErrStr))
}
return err
}
if err := cmd.Wait(); err != nil {
err := fmt.Errorf("waiting for the Azure CLI: %+v", err)
if stdErrStr := stderr.String(); stdErrStr != "" {
err = fmt.Errorf("%s: %s", err, strings.TrimSpace(stdErrStr))
}
return err
}
if err := json.Unmarshal([]byte(stdout.String()), &i); err != nil {
return fmt.Errorf("unmarshaling the result of Azure CLI: %v", err)
}
return nil
}