2023-06-01 21:20:10 +00:00
|
|
|
// Copyright 2023 The Prometheus Authors
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package azuread
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
"github.com/stretchr/testify/mock"
|
2023-10-05 02:16:36 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2023-06-01 21:20:10 +00:00
|
|
|
"github.com/stretchr/testify/suite"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-10-05 02:16:36 +00:00
|
|
|
dummyAudience = "dummyAudience"
|
|
|
|
dummyClientID = "00000000-0000-0000-0000-000000000000"
|
|
|
|
dummyClientSecret = "Cl1ent$ecret!"
|
|
|
|
dummyTenantID = "00000000-a12b-3cd4-e56f-000000000000"
|
|
|
|
testTokenString = "testTokenString"
|
2023-06-01 21:20:10 +00:00
|
|
|
)
|
|
|
|
|
2024-03-16 11:06:57 +00:00
|
|
|
func testTokenExpiry() time.Time { return time.Now().Add(5 * time.Second) }
|
2023-06-01 21:20:10 +00:00
|
|
|
|
|
|
|
type AzureAdTestSuite struct {
|
|
|
|
suite.Suite
|
|
|
|
mockCredential *mockCredential
|
|
|
|
}
|
|
|
|
|
|
|
|
type TokenProviderTestSuite struct {
|
|
|
|
suite.Suite
|
|
|
|
mockCredential *mockCredential
|
|
|
|
}
|
|
|
|
|
|
|
|
// mockCredential mocks azidentity TokenCredential interface.
|
|
|
|
type mockCredential struct {
|
|
|
|
mock.Mock
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ad *AzureAdTestSuite) BeforeTest(_, _ string) {
|
|
|
|
ad.mockCredential = new(mockCredential)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestAzureAd(t *testing.T) {
|
|
|
|
suite.Run(t, new(AzureAdTestSuite))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ad *AzureAdTestSuite) TestAzureAdRoundTripper() {
|
2023-10-05 02:16:36 +00:00
|
|
|
cases := []struct {
|
|
|
|
cfg *AzureADConfig
|
|
|
|
}{
|
|
|
|
// AzureAd roundtripper with Managedidentity.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "AzurePublic",
|
|
|
|
ManagedIdentity: &ManagedIdentityConfig{
|
|
|
|
ClientID: dummyClientID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// AzureAd roundtripper with OAuth.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "AzurePublic",
|
|
|
|
OAuth: &OAuthConfig{
|
|
|
|
ClientID: dummyClientID,
|
|
|
|
ClientSecret: dummyClientSecret,
|
|
|
|
TenantID: dummyTenantID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2023-06-01 21:20:10 +00:00
|
|
|
}
|
2023-10-05 02:16:36 +00:00
|
|
|
for _, c := range cases {
|
|
|
|
var gotReq *http.Request
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
testToken := &azcore.AccessToken{
|
|
|
|
Token: testTokenString,
|
2024-03-16 11:06:57 +00:00
|
|
|
ExpiresOn: testTokenExpiry(),
|
2023-10-05 02:16:36 +00:00
|
|
|
}
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
ad.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil)
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
tokenProvider, err := newTokenProvider(c.cfg, ad.mockCredential)
|
2023-12-07 11:35:01 +00:00
|
|
|
ad.Require().NoError(err)
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
rt := &azureADRoundTripper{
|
|
|
|
next: promhttp.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
|
|
|
|
gotReq = req
|
|
|
|
return &http.Response{StatusCode: http.StatusOK}, nil
|
|
|
|
}),
|
|
|
|
tokenProvider: tokenProvider,
|
|
|
|
}
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
cli := &http.Client{Transport: rt}
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
req, err := http.NewRequest(http.MethodPost, "https://example.com", strings.NewReader("Hello, world!"))
|
2023-12-07 11:35:01 +00:00
|
|
|
ad.Require().NoError(err)
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
_, err = cli.Do(req)
|
2023-12-07 11:35:01 +00:00
|
|
|
ad.Require().NoError(err)
|
|
|
|
ad.NotNil(gotReq)
|
2023-06-01 21:20:10 +00:00
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
origReq := gotReq
|
2023-12-07 11:35:01 +00:00
|
|
|
ad.NotEmpty(origReq.Header.Get("Authorization"))
|
|
|
|
ad.Equal("Bearer "+testTokenString, origReq.Header.Get("Authorization"))
|
2023-10-05 02:16:36 +00:00
|
|
|
}
|
2023-06-01 21:20:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func loadAzureAdConfig(filename string) (*AzureADConfig, error) {
|
|
|
|
content, err := os.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
cfg := AzureADConfig{}
|
|
|
|
if err = yaml.UnmarshalStrict(content, &cfg); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &cfg, nil
|
|
|
|
}
|
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
func TestAzureAdConfig(t *testing.T) {
|
|
|
|
cases := []struct {
|
|
|
|
filename string
|
|
|
|
err string
|
|
|
|
}{
|
|
|
|
// Missing managedidentiy or oauth field.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_bad_configmissing.yaml",
|
2024-03-16 11:06:57 +00:00
|
|
|
err: "must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config",
|
2023-10-05 02:16:36 +00:00
|
|
|
},
|
|
|
|
// Invalid managedidentity client id.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_bad_invalidclientid.yaml",
|
|
|
|
err: "the provided Azure Managed Identity client_id is invalid",
|
|
|
|
},
|
|
|
|
// Missing tenant id in oauth config.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_bad_invalidoauthconfig.yaml",
|
|
|
|
err: "must provide an Azure OAuth tenant_id in the Azure AD config",
|
|
|
|
},
|
|
|
|
// Invalid config when both managedidentity and oauth is provided.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_bad_twoconfig.yaml",
|
|
|
|
err: "cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config",
|
|
|
|
},
|
2024-03-16 11:06:57 +00:00
|
|
|
// Invalid config when both sdk and oauth is provided.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_bad_oauthsdkconfig.yaml",
|
|
|
|
err: "cannot provide both Azure OAuth and Azure SDK in the Azure AD config",
|
|
|
|
},
|
2023-10-05 02:16:36 +00:00
|
|
|
// Valid config with missing optionally cloud field.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_good_cloudmissing.yaml",
|
|
|
|
},
|
|
|
|
// Valid managed identity config.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_good_managedidentity.yaml",
|
|
|
|
},
|
|
|
|
// Valid Oauth config.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_good_oauth.yaml",
|
|
|
|
},
|
2024-03-16 11:06:57 +00:00
|
|
|
// Valid SDK config.
|
|
|
|
{
|
|
|
|
filename: "testdata/azuread_good_sdk.yaml",
|
|
|
|
},
|
2023-06-01 21:20:10 +00:00
|
|
|
}
|
2023-10-05 02:16:36 +00:00
|
|
|
for _, c := range cases {
|
|
|
|
_, err := loadAzureAdConfig(c.filename)
|
|
|
|
if c.err != "" {
|
|
|
|
if err == nil {
|
|
|
|
t.Fatalf("Did not receive expected error unmarshaling bad azuread config")
|
|
|
|
}
|
|
|
|
require.EqualError(t, err, c.err)
|
|
|
|
} else {
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2023-06-01 21:20:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockCredential) GetToken(ctx context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) {
|
|
|
|
args := m.MethodCalled("GetToken", ctx, options)
|
|
|
|
if args.Get(0) == nil {
|
|
|
|
return azcore.AccessToken{}, args.Error(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
return args.Get(0).(azcore.AccessToken), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *TokenProviderTestSuite) BeforeTest(_, _ string) {
|
|
|
|
s.mockCredential = new(mockCredential)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestTokenProvider(t *testing.T) {
|
|
|
|
suite.Run(t, new(TokenProviderTestSuite))
|
|
|
|
}
|
|
|
|
|
2023-10-05 02:16:36 +00:00
|
|
|
func (s *TokenProviderTestSuite) TestNewTokenProvider() {
|
|
|
|
cases := []struct {
|
|
|
|
cfg *AzureADConfig
|
|
|
|
err string
|
|
|
|
}{
|
|
|
|
// Invalid tokenProvider for managedidentity.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "PublicAzure",
|
|
|
|
ManagedIdentity: &ManagedIdentityConfig{
|
|
|
|
ClientID: dummyClientID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
err: "Cloud is not specified or is incorrect: ",
|
|
|
|
},
|
|
|
|
// Invalid tokenProvider for oauth.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "PublicAzure",
|
|
|
|
OAuth: &OAuthConfig{
|
|
|
|
ClientID: dummyClientID,
|
|
|
|
ClientSecret: dummyClientSecret,
|
|
|
|
TenantID: dummyTenantID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
err: "Cloud is not specified or is incorrect: ",
|
|
|
|
},
|
2024-03-16 11:06:57 +00:00
|
|
|
// Invalid tokenProvider for SDK.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "PublicAzure",
|
|
|
|
SDK: &SDKConfig{
|
|
|
|
TenantID: dummyTenantID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
err: "Cloud is not specified or is incorrect: ",
|
|
|
|
},
|
2023-10-05 02:16:36 +00:00
|
|
|
// Valid tokenProvider for managedidentity.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "AzurePublic",
|
|
|
|
ManagedIdentity: &ManagedIdentityConfig{
|
|
|
|
ClientID: dummyClientID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
// Valid tokenProvider for oauth.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "AzurePublic",
|
|
|
|
OAuth: &OAuthConfig{
|
|
|
|
ClientID: dummyClientID,
|
|
|
|
ClientSecret: dummyClientSecret,
|
|
|
|
TenantID: dummyTenantID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2024-03-16 11:06:57 +00:00
|
|
|
// Valid tokenProvider for SDK.
|
|
|
|
{
|
|
|
|
cfg: &AzureADConfig{
|
|
|
|
Cloud: "AzurePublic",
|
|
|
|
SDK: &SDKConfig{
|
|
|
|
TenantID: dummyTenantID,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2023-06-01 21:20:10 +00:00
|
|
|
}
|
2023-10-05 02:16:36 +00:00
|
|
|
mockGetTokenCallCounter := 1
|
|
|
|
for _, c := range cases {
|
|
|
|
if c.err != "" {
|
|
|
|
actualTokenProvider, actualErr := newTokenProvider(c.cfg, s.mockCredential)
|
|
|
|
|
2023-12-07 11:35:01 +00:00
|
|
|
s.Nil(actualTokenProvider)
|
|
|
|
s.Require().Error(actualErr)
|
|
|
|
s.Require().ErrorContains(actualErr, c.err)
|
2023-10-05 02:16:36 +00:00
|
|
|
} else {
|
|
|
|
testToken := &azcore.AccessToken{
|
|
|
|
Token: testTokenString,
|
2024-03-16 11:06:57 +00:00
|
|
|
ExpiresOn: testTokenExpiry(),
|
2023-10-05 02:16:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
s.mockCredential.On("GetToken", mock.Anything, mock.Anything).Return(*testToken, nil).Once().
|
2024-03-16 11:06:57 +00:00
|
|
|
On("GetToken", mock.Anything, mock.Anything).Return(getToken(), nil).Once()
|
2023-10-05 02:16:36 +00:00
|
|
|
|
|
|
|
actualTokenProvider, actualErr := newTokenProvider(c.cfg, s.mockCredential)
|
|
|
|
|
2023-12-07 11:35:01 +00:00
|
|
|
s.NotNil(actualTokenProvider)
|
|
|
|
s.Require().NoError(actualErr)
|
|
|
|
s.NotNil(actualTokenProvider.getAccessToken(context.Background()))
|
2023-10-05 02:16:36 +00:00
|
|
|
|
|
|
|
// Token set to refresh at half of the expiry time. The test tokens are set to expiry in 5s.
|
|
|
|
// Hence, the 4 seconds wait to check if the token is refreshed.
|
|
|
|
time.Sleep(4 * time.Second)
|
|
|
|
|
2023-12-07 11:35:01 +00:00
|
|
|
s.NotNil(actualTokenProvider.getAccessToken(context.Background()))
|
2023-10-05 02:16:36 +00:00
|
|
|
|
|
|
|
s.mockCredential.AssertNumberOfCalls(s.T(), "GetToken", 2*mockGetTokenCallCounter)
|
2023-11-29 17:23:34 +00:00
|
|
|
mockGetTokenCallCounter++
|
2023-10-05 02:16:36 +00:00
|
|
|
accessToken, err := actualTokenProvider.getAccessToken(context.Background())
|
2023-12-07 11:35:01 +00:00
|
|
|
s.Require().NoError(err)
|
|
|
|
s.NotEqual(testTokenString, accessToken)
|
2023-10-05 02:16:36 +00:00
|
|
|
}
|
2023-06-01 21:20:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func getToken() azcore.AccessToken {
|
|
|
|
return azcore.AccessToken{
|
|
|
|
Token: uuid.New().String(),
|
|
|
|
ExpiresOn: time.Now().Add(10 * time.Second),
|
|
|
|
}
|
|
|
|
}
|