Skip to content

Commit 4ad957a

Browse files
author
yanggang
committed
Add MSI Support for Azure plugin.
Signed-off-by: yanggang <[email protected]>
1 parent 9606df6 commit 4ad957a

File tree

3 files changed

+184
-4
lines changed

3 files changed

+184
-4
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add MSI Support for Azure plugin.

pkg/util/azure/credential.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626
"github.com/pkg/errors"
2727
)
2828

29-
// NewCredential chains the config credential and workload identity credential
29+
// NewCredential chains the config credential , workload identity credential , managed identity credential
3030
func NewCredential(creds map[string]string, options policy.ClientOptions) (azcore.TokenCredential, error) {
3131
var (
3232
credential []azcore.TokenCredential
@@ -60,6 +60,15 @@ func NewCredential(creds map[string]string, options policy.ClientOptions) (azcor
6060
errMsgs = append(errMsgs, err.Error())
6161
}
6262

63+
//managed identity credential
64+
o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: options, ID: azidentity.ClientID(creds[CredentialKeyClientID])}
65+
msi, err := azidentity.NewManagedIdentityCredential(o)
66+
if err == nil {
67+
credential = append(credential, msi)
68+
} else {
69+
errMsgs = append(errMsgs, err.Error())
70+
}
71+
6372
if len(credential) == 0 {
6473
return nil, errors.Errorf("failed to create Azure credential: %s", strings.Join(errMsgs, "\n\t"))
6574
}

pkg/util/azure/credential_test.go

Lines changed: 173 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,26 @@ limitations under the License.
1717
package azure
1818

1919
import (
20+
"bytes"
21+
"fmt"
22+
"io"
23+
"net/http"
24+
"strings"
2025
"testing"
2126

22-
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
27+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
2328
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
2429
"github.com/stretchr/testify/require"
2530
)
2631

2732
func TestNewCredential(t *testing.T) {
28-
options := policy.ClientOptions{}
33+
creds := map[string]string{}
34+
options := azcore.ClientOptions{Transport: &mockSTS{tenant: "",
35+
tokenRequestCallback: func(r *http.Request) {
36+
37+
}}}
2938

3039
// no credentials
31-
creds := map[string]string{}
3240
_, err := NewCredential(creds, options)
3341
require.NotNil(t, err)
3442

@@ -94,3 +102,165 @@ func Test_newConfigCredential(t *testing.T) {
94102
_, ok = credential.(*azidentity.UsernamePasswordCredential)
95103
require.True(t, ok)
96104
}
105+
106+
const (
107+
mockClientInfo = "eyJ1aWQiOiJjNzNjNmYyOC1hZTVmLTQxM2QtYTlhMi1lMTFlNWFmNjY4ZjgiLCJ1dGlkIjoiZTBiZDIzMjEtMDdmYS00Y2YwLTg3YjgtMDBhYTJhNzQ3MzI5In0"
108+
mockIDT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imwzc1EtNTBjQ0g0eEJWWkxIVEd3blNSNzY4MCJ9.eyJhdWQiOiIwNGIwNzc5NS04ZGRiLTQ2MWEtYmJlZS0wMmY5ZTFiZjdiNDYiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vYzU0ZmFjODgtM2RkMy00NjFmLWE3YzQtOGEzNjhlMDM0MGIzL3YyLjAiLCJpYXQiOjE2MzcxOTEyMTIsIm5iZiI6MTYzNzE5MTIxMiwiZXhwIjoxNjM3MTk1MTEyLCJhaW8iOiJBVVFBdS84VEFBQUFQMExOZGNRUXQxNmJoSkFreXlBdjFoUGJuQVhtT0o3RXJDVHV4N0hNTjhHd2VMb2FYMWR1cDJhQ2Y0a0p5bDFzNmovSzF5R05DZmVIQlBXM21QUWlDdz09IiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZTBiZDIzMjEtMDdmYS00Y2YwLTg3YjgtMDBhYTJhNzQ3MzI5LyIsIm5hbWUiOiJJZGVudGl0eSBUZXN0IFVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJpZGVudGl0eXRlc3R1c2VyQGF6dXJlc2Rrb3V0bG9vay5vbm1pY3Jvc29mdC5jb20iLCJyaCI6IjAuQVMwQWlLeFB4ZE05SDBhbnhJbzJqZ05BczVWM3NBVGJqUnBHdS00Qy1lR19lMFl0QUxFLiIsInN1YiI6ImMxYTBsY2xtbWxCYW9wc0MwVmlaLVpPMjFCT2dSUXE3SG9HRUtOOXloZnMiLCJ0aWQiOiJjNTRmYWM4OC0zZGQzLTQ2MWYtYTdjNC04YTM2OGUwMzQwYjMiLCJ1dGkiOiI5TXFOSWI5WjdrQy1QVHRtai11X0FBIiwidmVyIjoiMi4wIn0.hh5Exz9MBjTXrTuTZnz7vceiuQjcC_oRSTeBIC9tYgSO2c2sqQRpZi91qBZFQD9okayLPPKcwqXgEJD9p0-c4nUR5UQN7YSeDLmYtZUYMG79EsA7IMiQaiy94AyIe2E-oBDcLwFycGwh1iIOwwOwjbanmu2Dx3HfQx831lH9uVjagf0Aow0wTkTVCsedGSZvG-cRUceFLj-kFN-feFH3NuScuOfLR2Magf541pJda7X7oStwL_RNUFqjJFTdsiFV4e-VHK5qo--3oPU06z0rS9bosj0pFSATIVHrrS4gY7jiSvgMbG837CDBQkz5b08GUN5GlLN9jlygl1plBmbgww"
109+
110+
fakeClientID = "fake-client-id"
111+
fakeResourceID = "/fake/resource/ID"
112+
fakeTenantID = "fake-tenant"
113+
fakeUsername = "fake@user"
114+
fakeAdfsAuthority = "fake.adfs.local"
115+
fakeAdfsScope = "fake.adfs.local/fake-scope/.default"
116+
117+
tokenExpiresIn = 3600
118+
tokenValue = "new_token"
119+
)
120+
121+
var (
122+
accessTokenRespSuccess = []byte(fmt.Sprintf(`{"access_token": "%s", "expires_in": %d}`, tokenValue, tokenExpiresIn))
123+
)
124+
125+
func getInstanceDiscoveryResponse(tenant string) []byte {
126+
return []byte(strings.ReplaceAll(`{
127+
"tenant_discovery_endpoint": "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
128+
"api-version": "1.1",
129+
"metadata": [
130+
{
131+
"preferred_network": "login.microsoftonline.com",
132+
"preferred_cache": "login.windows.net",
133+
"aliases": [
134+
"login.microsoftonline.com",
135+
"login.windows.net",
136+
"login.microsoft.com",
137+
"sts.windows.net"
138+
]
139+
}
140+
]
141+
}`, "{tenant}", tenant))
142+
}
143+
144+
func getTenantDiscoveryResponse(tenant string) []byte {
145+
return []byte(strings.ReplaceAll(`{
146+
"token_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
147+
"token_endpoint_auth_methods_supported": [
148+
"client_secret_post",
149+
"private_key_jwt",
150+
"client_secret_basic"
151+
],
152+
"jwks_uri": "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys",
153+
"response_modes_supported": [
154+
"query",
155+
"fragment",
156+
"form_post"
157+
],
158+
"subject_types_supported": [
159+
"pairwise"
160+
],
161+
"id_token_signing_alg_values_supported": [
162+
"RS256"
163+
],
164+
"response_types_supported": [
165+
"code",
166+
"id_token",
167+
"code id_token",
168+
"id_token token"
169+
],
170+
"scopes_supported": [
171+
"openid",
172+
"profile",
173+
"email",
174+
"offline_access"
175+
],
176+
"issuer": "https://login.microsoftonline.com/{tenant}/v2.0",
177+
"request_uri_parameter_supported": false,
178+
"userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
179+
"authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",
180+
"device_authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode",
181+
"http_logout_supported": true,
182+
"frontchannel_logout_supported": true,
183+
"end_session_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/logout",
184+
"claims_supported": [
185+
"sub",
186+
"iss",
187+
"cloud_instance_name",
188+
"cloud_instance_host_name",
189+
"cloud_graph_host_name",
190+
"msgraph_host",
191+
"aud",
192+
"exp",
193+
"iat",
194+
"auth_time",
195+
"acr",
196+
"nonce",
197+
"preferred_username",
198+
"name",
199+
"tid",
200+
"ver",
201+
"at_hash",
202+
"c_hash",
203+
"email"
204+
],
205+
"kerberos_endpoint": "https://login.microsoftonline.com/{tenant}/kerberos",
206+
"tenant_region_scope": "NA",
207+
"cloud_instance_name": "microsoftonline.com",
208+
"cloud_graph_host_name": "graph.windows.net",
209+
"msgraph_host": "graph.microsoft.com",
210+
"rbac_url": "https://pas.windows.net"
211+
}`, "{tenant}", tenant))
212+
}
213+
214+
// mockSTS returns mock Azure AD responses so tests don't have to account for
215+
// MSAL metadata requests. All responses are success responses. Mock access
216+
// tokens expire in 1 hour and have the value of the "tokenValue" constant.
217+
type mockSTS struct {
218+
// tenant to include in metadata responses. This value must match a test's
219+
// expected tenant because metadata tells MSAL where to send token requests.
220+
// Defaults to the "fakeTenantID" constant.
221+
tenant string
222+
// tokenRequestCallback is called for every token request
223+
tokenRequestCallback func(*http.Request)
224+
}
225+
226+
func (m *mockSTS) Do(req *http.Request) (*http.Response, error) {
227+
res := http.Response{StatusCode: http.StatusOK}
228+
tenant := m.tenant
229+
if tenant == "" {
230+
tenant = fakeTenantID
231+
}
232+
switch s := strings.Split(req.URL.Path, "/"); s[len(s)-1] {
233+
case "instance":
234+
res.Body = io.NopCloser(bytes.NewReader(getInstanceDiscoveryResponse(tenant)))
235+
case "openid-configuration":
236+
res.Body = io.NopCloser(bytes.NewReader(getTenantDiscoveryResponse(tenant)))
237+
case "devicecode":
238+
res.Body = io.NopCloser(strings.NewReader(`{"device_code":"...","expires_in":600,"interval":60}`))
239+
case "token":
240+
if m.tokenRequestCallback != nil {
241+
m.tokenRequestCallback(req)
242+
}
243+
if err := req.ParseForm(); err != nil {
244+
return nil, fmt.Errorf("mockSTS failed to parse a request body: %w", err)
245+
}
246+
if grant := req.FormValue("grant_type"); grant == "device_code" || grant == "password" {
247+
// include account info because we're authenticating a user
248+
res.Body = io.NopCloser(bytes.NewReader(
249+
[]byte(fmt.Sprintf(`{"access_token":"at","expires_in": 3600,"refresh_token":"rt","client_info":%q,"id_token":%q}`, mockClientInfo, mockIDT)),
250+
))
251+
} else {
252+
res.Body = io.NopCloser(bytes.NewReader(accessTokenRespSuccess))
253+
}
254+
default:
255+
// User realm metadata request paths look like "/common/UserRealm/user@domain".
256+
// Matching on the UserRealm segment avoids having to know the UPN.
257+
if s[len(s)-2] == "UserRealm" {
258+
res.Body = io.NopCloser(
259+
strings.NewReader(`{"account_type":"Managed","cloud_audience_urn":"urn","cloud_instance_name":"...","domain_name":"..."}`),
260+
)
261+
} else {
262+
return nil, fmt.Errorf("mockSTS received an unexpected request for %s", req.URL.String())
263+
}
264+
}
265+
return &res, nil
266+
}

0 commit comments

Comments
 (0)