-
Notifications
You must be signed in to change notification settings - Fork 234
/
Copy pathazure_ps_context_credential.go
188 lines (160 loc) · 5.92 KB
/
azure_ps_context_credential.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
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package common
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
const credNamePSContext = "PSContextCredential"
type PSTokenProvider func(ctx context.Context, options policy.TokenRequestOptions) ([]byte, error)
func validTenantID(tenantID string) bool {
match, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", tenantID)
if err != nil {
return false
}
return match
}
func resolveTenant(defaultTenant, specified, credName string, additionalTenants []string) (string, error) {
if specified == "" || specified == defaultTenant {
return defaultTenant, nil
}
if defaultTenant == "adfs" {
return "", errors.New("ADFS doesn't support tenants")
}
if !validTenantID(specified) {
return "", errors.New("Invalid tenant")
}
for _, t := range additionalTenants {
if t == "*" || t == specified {
return specified, nil
}
}
return "", fmt.Errorf(`%s isn't configured to acquire tokens for tenant %q. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, credName, specified)
}
// PowershellContextCredentialOptions contains optional parameters for AzureDeveloperCLICredential.
type PowershellContextCredentialOptions struct {
// TenantID identifies the tenant the credential should authenticate in. Defaults to the azd environment,
// which is the tenant of the selected Azure subscription.
TenantID string
tokenProvider PSTokenProvider
}
// PowershellContextCredential authenticates as the identity logged in to the [Azure Developer CLI].
//
// [Azure Developer CLI]: https://learn.microsoft.com/azure/developer/azure-developer-cli/overview
type PowershellContextCredential struct {
mu *sync.Mutex
opts PowershellContextCredentialOptions
}
// NewPowershellContextCredential constructs an AzureDeveloperCLICredential. Pass nil to accept default options.
func NewPowershellContextCredential(options *PowershellContextCredentialOptions) (*PowershellContextCredential, error) {
cp := PowershellContextCredentialOptions{}
if options != nil {
cp = *options
}
if cp.TenantID != "" && !validTenantID(cp.TenantID) {
return nil, errors.New("invalid tenant id")
}
if cp.tokenProvider == nil {
cp.tokenProvider = defaultAzdTokenProvider
}
return &PowershellContextCredential{mu: &sync.Mutex{}, opts: cp}, nil
}
// GetToken requests a token from the Azure Developer CLI. This credential doesn't cache tokens, so every call invokes azd.
// This method is called automatically by Azure SDK clients.
func (c *PowershellContextCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
at := azcore.AccessToken{}
if len(opts.Scopes) != 1 {
return at, errors.New(credNamePSContext + ": GetToken() exactly one scope")
}
tenant, err := resolveTenant(c.opts.TenantID, opts.TenantID, credNamePSContext, nil)
if err != nil {
return at, err
}
c.mu.Lock()
defer c.mu.Unlock()
opts.TenantID = tenant
b, err := c.opts.tokenProvider(ctx, opts)
if err == nil {
at, err = c.createAccessToken(b)
}
if err != nil {
return at, err
}
//msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", credNamePSContext, strings.Join(opts.Scopes, ", "))
return at, nil
}
// We ignore resource because PS does not support all Resources. Disk scope is not supported
// and we are here only with Storage scope
var defaultAzdTokenProvider PSTokenProvider = func(ctx context.Context, opts policy.TokenRequestOptions) ([]byte, error) {
// set a default timeout for this authentication iff the application hasn't done so already
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
}
r := regexp.MustCompile("(?s){.*Token.*ExpiresOn.*}")
cmd := "Get-AzAccessToken"
// set options
if len(opts.Scopes) != 1 {
return nil, errors.New("exactly one scope must be specified")
} else {
cmd += fmt.Sprintf(" -ResourceUrl \"%s\"", strings.TrimSuffix(opts.Scopes[0], "/.default"))
}
if opts.TenantID != "" {
cmd += fmt.Sprintf(" -TenantId \"%s\"", opts.TenantID)
}
// We're going to get broken on this in Az 14.0 and Az.Accounts 5.0, so we may as well fix it now.
cmd += " -AsSecureString | Foreach-Object {[PSCustomObject]@{Token= $($_.Token | ConvertFrom-SecureString -AsPlainText); ExpiresOn = $_.ExpiresOn}} | ConvertTo-Json"
cliCmd := exec.CommandContext(ctx, "pwsh", "-Command", cmd)
cliCmd.Env = os.Environ()
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
msg := stderr.String()
if msg == "" {
msg = err.Error()
}
return nil, errors.New(credNamePSContext + msg)
}
output = []byte(r.FindString(string(output)))
if string(output) == "" {
invalidTokenMsg := " Invalid output received while retrieving token with Powershell. Run command \"" + cmd + "\"" +
" on powershell and verify that the output is indeed a valid token."
return nil, errors.New(credNamePSContext + invalidTokenMsg)
}
return output, nil
}
func (c *PowershellContextCredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
t := struct {
AccessToken string `json:"Token"`
ExpiresOn string `json:"ExpiresOn"`
}{}
err := json.Unmarshal(tk, &t)
if err != nil {
return azcore.AccessToken{}, errors.New(err.Error())
}
parseErr := "error parsing token expiration time %q: %v"
exp, err := time.Parse(time.RFC3339, t.ExpiresOn)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf(parseErr, t.ExpiresOn, err)
}
return azcore.AccessToken{
ExpiresOn: exp.UTC(),
Token: t.AccessToken,
}, nil
}
var _ azcore.TokenCredential = (*PowershellContextCredential)(nil)