diff --git a/pkg/azclient/auth.go b/pkg/azclient/auth.go index 7acbd1d927..df370eb042 100644 --- a/pkg/azclient/auth.go +++ b/pkg/azclient/auth.go @@ -17,156 +17,59 @@ limitations under the License. package azclient import ( - "context" + "errors" "fmt" - "os" "strings" "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/msi-dataplane/pkg/dataplane" +) - "sigs.k8s.io/cloud-provider-azure/pkg/azclient/armauth" +var ( + ErrNoValidAuthMethodFound = errors.New("no valid authentication method found") ) type AuthProvider struct { - ComputeCredential azcore.TokenCredential - NetworkCredential azcore.TokenCredential - MultiTenantCredential azcore.TokenCredential - CloudConfig cloud.Configuration + ComputeCredential azcore.TokenCredential + AdditionalComputeClientOptions []func(option *arm.ClientOptions) + NetworkCredential azcore.TokenCredential + CloudConfig cloud.Configuration } -func NewAuthProvider(armConfig *ARMClientConfig, config *AzureAuthConfig, clientOptionsMutFn ...func(option *policy.ClientOptions)) (*AuthProvider, error) { +func NewAuthProvider( + armConfig *ARMClientConfig, + config *AzureAuthConfig, + options ...AuthProviderOption, +) (*AuthProvider, error) { + opts := defaultAuthProviderOptions() + for _, opt := range options { + opt(opts) + } + clientOption, _, err := GetAzCoreClientOption(armConfig) if err != nil { return nil, err } - for _, fn := range clientOptionsMutFn { + for _, fn := range opts.ClientOptionsMutFn { fn(clientOption) } - var computeCredential azcore.TokenCredential - var networkTokenCredential azcore.TokenCredential - var multiTenantCredential azcore.TokenCredential - - // federatedIdentityCredential is used for workload identity federation - if aadFederatedTokenFile, enabled := config.GetAzureFederatedTokenFile(); enabled { - computeCredential, err = azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{ - ClientOptions: *clientOption, - ClientID: config.GetAADClientID(), - TenantID: armConfig.GetTenantID(), - TokenFilePath: aadFederatedTokenFile, - }) - if err != nil { - return nil, err - } - } - // managedIdentityCredential is used for managed identity extension - if computeCredential == nil && config.UseManagedIdentityExtension { - credOptions := &azidentity.ManagedIdentityCredentialOptions{ - ClientOptions: *clientOption, - } - if len(config.UserAssignedIdentityID) > 0 { - if strings.Contains(strings.ToUpper(config.UserAssignedIdentityID), "/SUBSCRIPTIONS/") { - credOptions.ID = azidentity.ResourceID(config.UserAssignedIdentityID) - } else { - credOptions.ID = azidentity.ClientID(config.UserAssignedIdentityID) - } - } - computeCredential, err = azidentity.NewManagedIdentityCredential(credOptions) - if err != nil { - return nil, err - } - if config.AuxiliaryTokenProvider != nil && IsMultiTenant(armConfig) { - networkTokenCredential, err = armauth.NewKeyVaultCredential( - computeCredential, - config.AuxiliaryTokenProvider.SecretResourceID(), - ) - if err != nil { - return nil, fmt.Errorf("create KeyVaultCredential for auxiliary token provider: %w", err) - } - } - } - - // Client secret authentication - if computeCredential == nil && len(config.GetAADClientSecret()) > 0 { - credOptions := &azidentity.ClientSecretCredentialOptions{ - ClientOptions: *clientOption, - } - computeCredential, err = azidentity.NewClientSecretCredential(armConfig.GetTenantID(), config.GetAADClientID(), config.GetAADClientSecret(), credOptions) - if err != nil { - return nil, err - } - if IsMultiTenant(armConfig) { - credOptions := &azidentity.ClientSecretCredentialOptions{ - ClientOptions: *clientOption, - } - networkTokenCredential, err = azidentity.NewClientSecretCredential(armConfig.NetworkResourceTenantID, config.GetAADClientID(), config.AADClientSecret, credOptions) - if err != nil { - return nil, err - } - - credOptions = &azidentity.ClientSecretCredentialOptions{ - ClientOptions: *clientOption, - AdditionallyAllowedTenants: []string{armConfig.NetworkResourceTenantID}, - } - multiTenantCredential, err = azidentity.NewClientSecretCredential(armConfig.GetTenantID(), config.GetAADClientID(), config.GetAADClientSecret(), credOptions) - if err != nil { - return nil, err - } - - } - } - // ClientCertificateCredential is used for client certificate - if computeCredential == nil && len(config.AADClientCertPath) > 0 { - credOptions := &azidentity.ClientCertificateCredentialOptions{ - ClientOptions: *clientOption, - SendCertificateChain: true, - } - certData, err := os.ReadFile(config.AADClientCertPath) - if err != nil { - return nil, fmt.Errorf("reading the client certificate from file %s: %w", config.AADClientCertPath, err) - } - certificate, privateKey, err := azidentity.ParseCertificates(certData, []byte(config.AADClientCertPassword)) - if err != nil { - return nil, fmt.Errorf("decoding the client certificate: %w", err) - } - computeCredential, err = azidentity.NewClientCertificateCredential(armConfig.GetTenantID(), config.GetAADClientID(), certificate, privateKey, credOptions) - if err != nil { - return nil, err - } - if IsMultiTenant(armConfig) { - networkTokenCredential, err = azidentity.NewClientCertificateCredential(armConfig.NetworkResourceTenantID, config.GetAADClientID(), certificate, privateKey, credOptions) - if err != nil { - return nil, err - } - credOptions = &azidentity.ClientCertificateCredentialOptions{ - ClientOptions: *clientOption, - AdditionallyAllowedTenants: []string{armConfig.NetworkResourceTenantID}, - } - multiTenantCredential, err = azidentity.NewClientCertificateCredential(armConfig.GetTenantID(), config.GetAADClientID(), certificate, privateKey, credOptions) - if err != nil { - return nil, err - } - } + aadFederatedTokenFile, federatedTokenEnabled := config.GetAzureFederatedTokenFile() + switch { + case federatedTokenEnabled: + return newAuthProviderWithWorkloadIdentity(aadFederatedTokenFile, armConfig, config, clientOption, opts) + case config.UseManagedIdentityExtension: + return newAuthProviderWithManagedIdentity(armConfig, config, clientOption, opts) + case len(config.GetAADClientSecret()) > 0: + return newAuthProviderWithServicePrincipalClientSecret(armConfig, config, clientOption, opts) + case len(config.AADClientCertPath) > 0: + return newAuthProviderWithServicePrincipalClientCertificate(armConfig, config, clientOption, opts) + case len(config.AADMSIDataPlaneIdentityPath) > 0: + return newAuthProviderWithUserAssignedIdentity(config, clientOption, opts) + default: + return nil, ErrNoValidAuthMethodFound } - - // UserAssignedIdentityCredentials authentication - if computeCredential == nil && len(config.AADMSIDataPlaneIdentityPath) > 0 { - computeCredential, err = dataplane.NewUserAssignedIdentityCredential(context.Background(), config.AADMSIDataPlaneIdentityPath, dataplane.WithClientOpts(azcore.ClientOptions{Cloud: clientOption.Cloud})) - if err != nil { - return nil, err - } - } - - return &AuthProvider{ - ComputeCredential: computeCredential, - NetworkCredential: networkTokenCredential, - MultiTenantCredential: multiTenantCredential, - CloudConfig: clientOption.Cloud, - }, nil } func (factory *AuthProvider) GetAzIdentity() azcore.TokenCredential { @@ -180,18 +83,11 @@ func (factory *AuthProvider) GetNetworkAzIdentity() azcore.TokenCredential { return factory.ComputeCredential } -func (factory *AuthProvider) GetMultiTenantIdentity() azcore.TokenCredential { - if factory.MultiTenantCredential != nil { - return factory.MultiTenantCredential - } - return factory.ComputeCredential -} - -func (factory *AuthProvider) IsMultiTenantModeEnabled() bool { - return factory.MultiTenantCredential != nil +func (factory *AuthProvider) DefaultTokenScope() string { + return DefaultTokenScopeFor(factory.CloudConfig) } -func (factory *AuthProvider) DefaultTokenScope() string { - audience := factory.CloudConfig.Services[cloud.ResourceManager].Audience +func DefaultTokenScopeFor(cloudCfg cloud.Configuration) string { + audience := cloudCfg.Services[cloud.ResourceManager].Audience return fmt.Sprintf("%s/.default", strings.TrimRight(audience, "/")) } diff --git a/pkg/azclient/auth_fake_test.go b/pkg/azclient/auth_fake_test.go new file mode 100644 index 0000000000..77586f37fd --- /dev/null +++ b/pkg/azclient/auth_fake_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2025 The Kubernetes 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 azclient + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/stretchr/testify/assert" +) + +var ( + incCounter = atomic.Int64{} +) + +type fakeTokenCredential struct { + ID string +} + +func newFakeTokenCredential() *fakeTokenCredential { + id := fmt.Sprintf("fake-token-credential-%d-%d", incCounter.Add(1), time.Now().UnixNano()) + return &fakeTokenCredential{ID: id} +} + +func (f *fakeTokenCredential) GetToken( + _ context.Context, + _ policy.TokenRequestOptions, +) (azcore.AccessToken, error) { + panic("not implemented") +} + +type AuthProviderAssertions func(t testing.TB, authProvider *AuthProvider) + +func ApplyAssertions(t testing.TB, authProvider *AuthProvider, assertions []AuthProviderAssertions) { + t.Helper() + + for _, assertion := range assertions { + assertion(t, authProvider) + } +} + +func AssertComputeTokenCredential(tokenCredential *fakeTokenCredential) AuthProviderAssertions { + return func(t testing.TB, authProvider *AuthProvider) { + t.Helper() + + assert.NotNil(t, authProvider.ComputeCredential) + + cred, ok := authProvider.ComputeCredential.(*fakeTokenCredential) + assert.True(t, ok, "expected a fake token credential") + assert.Equal(t, tokenCredential.ID, cred.ID) + } +} + +func AssertNetworkTokenCredential(tokenCredential *fakeTokenCredential) AuthProviderAssertions { + return func(t testing.TB, authProvider *AuthProvider) { + t.Helper() + + assert.NotNil(t, authProvider.NetworkCredential) + + cred, ok := authProvider.NetworkCredential.(*fakeTokenCredential) + assert.True(t, ok, "expected a fake token credential") + assert.Equal(t, tokenCredential.ID, cred.ID) + } +} + +func AssertNilNetworkTokenCredential() AuthProviderAssertions { + return func(t testing.TB, authProvider *AuthProvider) { + t.Helper() + + assert.Nil(t, authProvider.NetworkCredential) + } +} + +func AssertEmptyAdditionalComputeClientOptions() AuthProviderAssertions { + return func(t testing.TB, authProvider *AuthProvider) { + t.Helper() + + assert.Empty(t, authProvider.AdditionalComputeClientOptions) + } +} + +func AssertCloudConfig(expected cloud.Configuration) AuthProviderAssertions { + return func(t testing.TB, authProvider *AuthProvider) { + t.Helper() + + assert.Equal(t, expected, authProvider.CloudConfig) + } +} diff --git a/pkg/azclient/auth_func.go b/pkg/azclient/auth_func.go new file mode 100644 index 0000000000..31e6e2c819 --- /dev/null +++ b/pkg/azclient/auth_func.go @@ -0,0 +1,310 @@ +/* +Copyright 2025 The Kubernetes 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 azclient + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/msi-dataplane/pkg/dataplane" + + "sigs.k8s.io/cloud-provider-azure/pkg/azclient/armauth" +) + +var ( + ErrAuxiliaryTokenProviderNotSet = errors.New("auxiliary token provider is not set when multi-tenant is enabled for MSI") + ErrNewKeyVaultCredentialFailed = errors.New("create KeyVaultCredential failed") +) + +// newAuthProviderWithWorkloadIdentity creates a new AuthProvider with workload identity. +// The caller is responsible for checking if workload identity is enabled. +// NOTE: it does NOT support multi-tenant scenarios. +func newAuthProviderWithWorkloadIdentity( + aadFederatedTokenFile string, + armConfig *ARMClientConfig, + config *AzureAuthConfig, + clientOptions *policy.ClientOptions, + opts *authProviderOptions, +) (*AuthProvider, error) { + computeCredential, err := opts.NewWorkloadIdentityCredentialFn(&azidentity.WorkloadIdentityCredentialOptions{ + ClientOptions: *clientOptions, + ClientID: config.GetAADClientID(), + TenantID: armConfig.GetTenantID(), + TokenFilePath: aadFederatedTokenFile, + }) + if err != nil { + return nil, err + } + + return &AuthProvider{ + ComputeCredential: computeCredential, + CloudConfig: clientOptions.Cloud, + }, nil +} + +// newAuthProviderWithManagedIdentity creates a new AuthProvider with managed identity. +// When multi-tenant is enabled, it uses the auxiliary token provider to create a network credential +// for cross-tenant resource access. If multi-tenant is enabled but the auxiliary token provider +// is not configured, it returns an error. +func newAuthProviderWithManagedIdentity( + armConfig *ARMClientConfig, + config *AzureAuthConfig, + clientOptions *policy.ClientOptions, + opts *authProviderOptions, +) (*AuthProvider, error) { + credOptions := &azidentity.ManagedIdentityCredentialOptions{ + ClientOptions: *clientOptions, + } + if len(config.UserAssignedIdentityID) > 0 { + if strings.Contains(strings.ToUpper(config.UserAssignedIdentityID), "/SUBSCRIPTIONS/") { + credOptions.ID = azidentity.ResourceID(config.UserAssignedIdentityID) + } else { + credOptions.ID = azidentity.ClientID(config.UserAssignedIdentityID) + } + } + + computeCredential, err := opts.NewManagedIdentityCredentialFn(credOptions) + if err != nil { + return nil, err + } + + rv := &AuthProvider{ + ComputeCredential: computeCredential, + CloudConfig: clientOptions.Cloud, + } + + if !IsMultiTenant(armConfig) { + return rv, nil + } + + if config.AuxiliaryTokenProvider == nil { + return nil, ErrAuxiliaryTokenProviderNotSet + } + + // Use AuxiliaryTokenProvider as the network credential + networkCredential, err := opts.NewKeyVaultCredentialFn( + computeCredential, + config.AuxiliaryTokenProvider.SecretResourceID(), + ) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrNewKeyVaultCredentialFailed, err) + } + + // Additionally, we need to add the auxiliary token to the HTTP header when making requests to the compute resources + additionalComputeClientOptions := []func(option *arm.ClientOptions){ + func(option *arm.ClientOptions) { + option.PerRetryPolicies = append(option.PerRetryPolicies, armauth.NewAuxiliaryAuthPolicy( + []azcore.TokenCredential{networkCredential}, + DefaultTokenScopeFor(clientOptions.Cloud), + )) + }, + } + rv.NetworkCredential = networkCredential + rv.AdditionalComputeClientOptions = additionalComputeClientOptions + return rv, nil +} + +// newAuthProviderWithServicePrincipalClientSecret creates a new AuthProvider with service principal client secret. +// When multi-tenant is enabled, it creates a compute credential with additional allowed tenants for cross-tenant access. +func newAuthProviderWithServicePrincipalClientSecret( + armConfig *ARMClientConfig, + config *AzureAuthConfig, + clientOptions *policy.ClientOptions, + opts *authProviderOptions, +) (*AuthProvider, error) { + var ( + computeCredential azcore.TokenCredential + networkCredential azcore.TokenCredential + ) + + if !IsMultiTenant(armConfig) { + // Single tenant + credOptions := &azidentity.ClientSecretCredentialOptions{ + ClientOptions: *clientOptions, + } + var err error + computeCredential, err = opts.NewClientSecretCredentialFn( + armConfig.GetTenantID(), + config.GetAADClientID(), + config.GetAADClientSecret(), + credOptions, + ) + if err != nil { + return nil, err + } + + return &AuthProvider{ + ComputeCredential: computeCredential, + CloudConfig: clientOptions.Cloud, + }, nil + } + + // Network credential for network resource access + { + credOptions := &azidentity.ClientSecretCredentialOptions{ + ClientOptions: *clientOptions, + } + var err error + networkCredential, err = opts.NewClientSecretCredentialFn( + armConfig.NetworkResourceTenantID, + config.GetAADClientID(), + config.GetAADClientSecret(), + credOptions, + ) + if err != nil { + return nil, err + } + } + + // Compute credential with additional allowed tenants for cross-tenant access + { + credOptions := &azidentity.ClientSecretCredentialOptions{ + ClientOptions: *clientOptions, + AdditionallyAllowedTenants: []string{armConfig.NetworkResourceTenantID}, + } + var err error + computeCredential, err = opts.NewClientSecretCredentialFn( + armConfig.GetTenantID(), + config.GetAADClientID(), + config.GetAADClientSecret(), + credOptions, + ) + if err != nil { + return nil, err + } + } + + return &AuthProvider{ + ComputeCredential: computeCredential, + NetworkCredential: networkCredential, + CloudConfig: clientOptions.Cloud, + }, nil +} + +// newAuthProviderWithServicePrincipalClientCertificate creates a new AuthProvider with service principal client certificate. +// When multi-tenant is enabled, it creates a compute credential with additional allowed tenants for cross-tenant access. +func newAuthProviderWithServicePrincipalClientCertificate( + armConfig *ARMClientConfig, + config *AzureAuthConfig, + clientOptions *policy.ClientOptions, + opts *authProviderOptions, +) (*AuthProvider, error) { + certData, err := opts.ReadFileFn(config.AADClientCertPath) + if err != nil { + return nil, fmt.Errorf("reading the client certificate from file %s: %w", config.AADClientCertPath, err) + } + certificate, privateKey, err := opts.ParseCertificatesFn(certData, []byte(config.AADClientCertPassword)) + if err != nil { + return nil, fmt.Errorf("decoding the client certificate: %w", err) + } + + var ( + computeCredential azcore.TokenCredential + networkCredential azcore.TokenCredential + ) + + if !IsMultiTenant(armConfig) { + // Single tenant + credOptions := &azidentity.ClientCertificateCredentialOptions{ + ClientOptions: *clientOptions, + SendCertificateChain: true, + } + computeCredential, err = opts.NewClientCertificateCredentialFn( + armConfig.GetTenantID(), + config.GetAADClientID(), + certificate, + privateKey, + credOptions, + ) + if err != nil { + return nil, err + } + return &AuthProvider{ + ComputeCredential: computeCredential, + CloudConfig: clientOptions.Cloud, + }, nil + } + + // Network credential for network resource access + { + credOptions := &azidentity.ClientCertificateCredentialOptions{ + ClientOptions: *clientOptions, + SendCertificateChain: true, + } + networkCredential, err = opts.NewClientCertificateCredentialFn( + armConfig.NetworkResourceTenantID, + config.GetAADClientID(), + certificate, + privateKey, + credOptions, + ) + if err != nil { + return nil, err + } + } + + // Compute credential with additional allowed tenants for cross-tenant access + { + credOptions := &azidentity.ClientCertificateCredentialOptions{ + ClientOptions: *clientOptions, + AdditionallyAllowedTenants: []string{armConfig.NetworkResourceTenantID}, + SendCertificateChain: true, + } + computeCredential, err = opts.NewClientCertificateCredentialFn( + armConfig.GetTenantID(), + config.GetAADClientID(), + certificate, + privateKey, + credOptions, + ) + if err != nil { + return nil, err + } + } + + return &AuthProvider{ + ComputeCredential: computeCredential, + NetworkCredential: networkCredential, + CloudConfig: clientOptions.Cloud, + }, nil +} + +func newAuthProviderWithUserAssignedIdentity( + config *AzureAuthConfig, + clientOptions *policy.ClientOptions, + opts *authProviderOptions, +) (*AuthProvider, error) { + computeCredential, err := opts.NewUserAssignedIdentityCredentialFn( + context.Background(), + config.AADMSIDataPlaneIdentityPath, + dataplane.WithClientOpts(azcore.ClientOptions{Cloud: clientOptions.Cloud}), + ) + if err != nil { + return nil, err + } + + return &AuthProvider{ + ComputeCredential: computeCredential, + CloudConfig: clientOptions.Cloud, + }, nil +} diff --git a/pkg/azclient/auth_func_test.go b/pkg/azclient/auth_func_test.go new file mode 100644 index 0000000000..68c7bec95d --- /dev/null +++ b/pkg/azclient/auth_func_test.go @@ -0,0 +1,777 @@ +/* +Copyright 2025 The Kubernetes 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 azclient + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/msi-dataplane/pkg/dataplane" + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" + + "sigs.k8s.io/cloud-provider-azure/pkg/azclient/armauth" +) + +func TestNewAuthProviderWithWorkloadIdentity(t *testing.T) { + t.Parallel() + + var ( + testAADClientID = faker.UUIDHyphenated() + testTenantID = faker.UUIDHyphenated() + testTokenFileName = faker.Word() + testARMConfig = &ARMClientConfig{ + TenantID: testTenantID, + } + testAzureAuthConfig = &AzureAuthConfig{ + AADClientID: testAADClientID, + } + testCloudConfig = cloud.AzurePublic + testClientOption = &policy.ClientOptions{Cloud: testCloudConfig} + testFakeComputeTokenCredential = newFakeTokenCredential() + testErr = errors.New("test error") + ) + + tests := []struct { + Name string + AADFederatedTokenFile string + ARMConfig *ARMClientConfig + AuthConfig *AzureAuthConfig + ClientOption *policy.ClientOptions + Opts *authProviderOptions + Assertions []AuthProviderAssertions + ExpectErr error + }{ + { + Name: "error when creating workload identity credential", + AADFederatedTokenFile: testTokenFileName, + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewWorkloadIdentityCredentialFn: func(_ *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) { + return nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "success", + AADFederatedTokenFile: testTokenFileName, + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewWorkloadIdentityCredentialFn: func(options *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.Equal(t, testAADClientID, options.ClientID) + assert.Equal(t, testTenantID, options.TenantID) + assert.Equal(t, testTokenFileName, options.TokenFilePath) + + return testFakeComputeTokenCredential, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + authProvider, err := newAuthProviderWithWorkloadIdentity( + tt.AADFederatedTokenFile, + tt.ARMConfig, + tt.AuthConfig, + tt.ClientOption, + tt.Opts, + ) + + if tt.ExpectErr != nil { + assert.Error(t, err) + assert.ErrorIs(t, err, tt.ExpectErr) + } else { + assert.NoError(t, err) + ApplyAssertions(t, authProvider, tt.Assertions) + } + }) + } +} + +func TestNewAuthProviderWithManagedIdentity(t *testing.T) { + t.Parallel() + + var ( + testTenantID = faker.UUIDHyphenated() + testNetworkTenantID = faker.UUIDHyphenated() + testUserAssignedIdentityID = faker.UUIDHyphenated() + testARMConfig = &ARMClientConfig{ + TenantID: testTenantID, + } + testARMConfigMultiTenant = &ARMClientConfig{ + TenantID: testTenantID, + NetworkResourceTenantID: testNetworkTenantID, + } + testAzureAuthConfig = &AzureAuthConfig{ + UserAssignedIdentityID: testUserAssignedIdentityID, + } + testAzureAuthConfigWithAuxiliaryProvider = &AzureAuthConfig{ + UserAssignedIdentityID: testUserAssignedIdentityID, + AuxiliaryTokenProvider: &AzureAuthAuxiliaryTokenProvider{ + SubscriptionID: faker.UUIDHyphenated(), + ResourceGroup: faker.Word(), + VaultName: faker.Word(), + SecretName: faker.Word(), + }, + } + testCloudConfig = cloud.AzurePublic + testClientOption = &policy.ClientOptions{Cloud: testCloudConfig} + testFakeComputeTokenCredential = newFakeTokenCredential() + testFakeNetworkTokenCredential = newFakeTokenCredential() + testErr = errors.New("test error") + ) + + tests := []struct { + Name string + ARMConfig *ARMClientConfig + AuthConfig *AzureAuthConfig + ClientOption *policy.ClientOptions + Opts *authProviderOptions + Assertions []AuthProviderAssertions + ExpectErr error + }{ + { + Name: "error when creating managed identity credential", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewManagedIdentityCredentialFn: func(_ *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + return nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "success with single tenant", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewManagedIdentityCredentialFn: func(options *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.Equal(t, azidentity.ClientID(testUserAssignedIdentityID), options.ID) + return testFakeComputeTokenCredential, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "error with multi-tenant and no auxiliary token provider", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewManagedIdentityCredentialFn: func(_ *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + return testFakeComputeTokenCredential, nil + }, + }, + ExpectErr: ErrAuxiliaryTokenProviderNotSet, + }, + { + Name: "error with multi-tenant when creating KeyVault credential", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfigWithAuxiliaryProvider, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewManagedIdentityCredentialFn: func(_ *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + return testFakeComputeTokenCredential, nil + }, + NewKeyVaultCredentialFn: func(_ azcore.TokenCredential, _ armauth.SecretResourceID) (azcore.TokenCredential, error) { + return nil, testErr + }, + }, + ExpectErr: ErrNewKeyVaultCredentialFailed, + }, + { + Name: "success with multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfigWithAuxiliaryProvider, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewManagedIdentityCredentialFn: func(options *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.Equal(t, azidentity.ClientID(testUserAssignedIdentityID), options.ID) + return testFakeComputeTokenCredential, nil + }, + NewKeyVaultCredentialFn: func(credential azcore.TokenCredential, secretResourceID armauth.SecretResourceID) (azcore.TokenCredential, error) { + assert.Equal(t, testFakeComputeTokenCredential, credential) + assert.Equal(t, testAzureAuthConfigWithAuxiliaryProvider.AuxiliaryTokenProvider.SecretResourceID(), secretResourceID) + return testFakeNetworkTokenCredential, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNetworkTokenCredential(testFakeNetworkTokenCredential), + AssertCloudConfig(testCloudConfig), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + authProvider, err := newAuthProviderWithManagedIdentity( + tt.ARMConfig, + tt.AuthConfig, + tt.ClientOption, + tt.Opts, + ) + + if tt.ExpectErr != nil { + assert.Error(t, err) + if errors.Is(err, tt.ExpectErr) { + assert.ErrorIs(t, err, tt.ExpectErr) + } else { + assert.ErrorContains(t, err, tt.ExpectErr.Error()) + } + } else { + assert.NoError(t, err) + ApplyAssertions(t, authProvider, tt.Assertions) + } + }) + } +} + +func TestNewAuthProviderWithServicePrincipalClientSecret(t *testing.T) { + t.Parallel() + + var ( + testAADClientID = faker.UUIDHyphenated() + testAADClientSecret = faker.Password() + testTenantID = faker.UUIDHyphenated() + testNetworkTenantID = faker.UUIDHyphenated() + testARMConfig = &ARMClientConfig{ + TenantID: testTenantID, + } + testARMConfigMultiTenant = &ARMClientConfig{ + TenantID: testTenantID, + NetworkResourceTenantID: testNetworkTenantID, + } + testAzureAuthConfig = &AzureAuthConfig{ + AADClientID: testAADClientID, + AADClientSecret: testAADClientSecret, + } + testCloudConfig = cloud.AzurePublic + testClientOption = &policy.ClientOptions{Cloud: testCloudConfig} + testFakeComputeTokenCredential = newFakeTokenCredential() + testFakeNetworkTokenCredential = newFakeTokenCredential() + testErr = errors.New("test error") + ) + + tests := []struct { + Name string + ARMConfig *ARMClientConfig + AuthConfig *AzureAuthConfig + ClientOption *policy.ClientOptions + Opts *authProviderOptions + Assertions []AuthProviderAssertions + ExpectErr error + }{ + { + Name: "error when creating client secret credential in single tenant", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewClientSecretCredentialFn: func(_ string, _ string, _ string, _ *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + return nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "success with single tenant", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewClientSecretCredentialFn: func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.Empty(t, options.AdditionallyAllowedTenants) + return testFakeComputeTokenCredential, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "error when creating network credential in multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewClientSecretCredentialFn: func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + if tenantID == testNetworkTenantID { + return nil, testErr + } + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.Contains(t, options.AdditionallyAllowedTenants, testNetworkTenantID) + return testFakeComputeTokenCredential, nil + }, + }, + ExpectErr: testErr, + }, + { + Name: "error when creating compute credential in multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewClientSecretCredentialFn: func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + if tenantID == testTenantID { + return nil, testErr + } + assert.Equal(t, testNetworkTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.Empty(t, options.AdditionallyAllowedTenants) + return testFakeNetworkTokenCredential, nil + }, + }, + ExpectErr: testErr, + }, + { + Name: "success with multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewClientSecretCredentialFn: func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, *testClientOption, options.ClientOptions) + + if tenantID == testNetworkTenantID { + assert.Empty(t, options.AdditionallyAllowedTenants) + return testFakeNetworkTokenCredential, nil + } else if tenantID == testTenantID { + assert.Contains(t, options.AdditionallyAllowedTenants, testNetworkTenantID) + return testFakeComputeTokenCredential, nil + } + + t.Fatalf("unexpected tenant ID: %s", tenantID) + return nil, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNetworkTokenCredential(testFakeNetworkTokenCredential), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + authProvider, err := newAuthProviderWithServicePrincipalClientSecret( + tt.ARMConfig, + tt.AuthConfig, + tt.ClientOption, + tt.Opts, + ) + + if tt.ExpectErr != nil { + assert.Error(t, err) + assert.ErrorIs(t, err, tt.ExpectErr) + } else { + assert.NoError(t, err) + ApplyAssertions(t, authProvider, tt.Assertions) + } + }) + } +} + +func TestNewAuthProviderWithServicePrincipalClientCertificate(t *testing.T) { + t.Parallel() + + var ( + testAADClientID = faker.UUIDHyphenated() + testAADClientCertPath = faker.Word() + testAADClientCertPassword = faker.Password() + testTenantID = faker.UUIDHyphenated() + testNetworkTenantID = faker.UUIDHyphenated() + testARMConfig = &ARMClientConfig{ + TenantID: testTenantID, + } + testARMConfigMultiTenant = &ARMClientConfig{ + TenantID: testTenantID, + NetworkResourceTenantID: testNetworkTenantID, + } + testAzureAuthConfig = &AzureAuthConfig{ + AADClientID: testAADClientID, + AADClientCertPath: testAADClientCertPath, + AADClientCertPassword: testAADClientCertPassword, + } + testCloudConfig = cloud.AzurePublic + testClientOption = &policy.ClientOptions{Cloud: testCloudConfig} + testFakeComputeTokenCredential = newFakeTokenCredential() + testFakeNetworkTokenCredential = newFakeTokenCredential() + testErr = errors.New("test error") + testCertData = []byte(faker.Word()) + testCerts = []*x509.Certificate{{}} + testPrivateKey = struct{ crypto.PrivateKey }{} + ) + + tests := []struct { + Name string + ARMConfig *ARMClientConfig + AuthConfig *AzureAuthConfig + ClientOption *policy.ClientOptions + Opts *authProviderOptions + Assertions []AuthProviderAssertions + ExpectErr error + }{ + { + Name: "error reading certificate file", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "error parsing certificate", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + }, + ParseCertificatesFn: func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return nil, nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "error creating client certificate credential in single tenant", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + }, + ParseCertificatesFn: func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return testCerts, testPrivateKey, nil + }, + NewClientCertificateCredentialFn: func(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCerts, certs) + assert.Equal(t, testPrivateKey, key) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.True(t, options.SendCertificateChain) + return nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "success with single tenant", + ARMConfig: testARMConfig, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + }, + ParseCertificatesFn: func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return testCerts, testPrivateKey, nil + }, + NewClientCertificateCredentialFn: func(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCerts, certs) + assert.Equal(t, testPrivateKey, key) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.True(t, options.SendCertificateChain) + assert.Empty(t, options.AdditionallyAllowedTenants) + return testFakeComputeTokenCredential, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "error when creating network credential in multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + }, + ParseCertificatesFn: func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return testCerts, testPrivateKey, nil + }, + NewClientCertificateCredentialFn: func(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) { + if tenantID == testNetworkTenantID { + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCerts, certs) + assert.Equal(t, testPrivateKey, key) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.True(t, options.SendCertificateChain) + return nil, testErr + } + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCerts, certs) + assert.Equal(t, testPrivateKey, key) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.True(t, options.SendCertificateChain) + return testFakeComputeTokenCredential, nil + }, + }, + ExpectErr: testErr, + }, + { + Name: "error when creating compute credential in multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + }, + ParseCertificatesFn: func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return testCerts, testPrivateKey, nil + }, + NewClientCertificateCredentialFn: func(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) { + if tenantID == testTenantID { + return nil, testErr + } + assert.Equal(t, testNetworkTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCerts, certs) + assert.Equal(t, testPrivateKey, key) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.True(t, options.SendCertificateChain) + return testFakeNetworkTokenCredential, nil + }, + }, + ExpectErr: testErr, + }, + { + Name: "success with multi-tenant", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + ReadFileFn: func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + }, + ParseCertificatesFn: func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return testCerts, testPrivateKey, nil + }, + NewClientCertificateCredentialFn: func(tenantID string, clientID string, certs []*x509.Certificate, key crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCerts, certs) + assert.Equal(t, testPrivateKey, key) + assert.Equal(t, *testClientOption, options.ClientOptions) + assert.True(t, options.SendCertificateChain) + + if tenantID == testNetworkTenantID { + assert.Empty(t, options.AdditionallyAllowedTenants) + return testFakeNetworkTokenCredential, nil + } else if tenantID == testTenantID { + assert.Contains(t, options.AdditionallyAllowedTenants, testNetworkTenantID) + return testFakeComputeTokenCredential, nil + } + + t.Fatalf("unexpected tenant ID: %s", tenantID) + return nil, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNetworkTokenCredential(testFakeNetworkTokenCredential), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + authProvider, err := newAuthProviderWithServicePrincipalClientCertificate( + tt.ARMConfig, + tt.AuthConfig, + tt.ClientOption, + tt.Opts, + ) + + if tt.ExpectErr != nil { + assert.Error(t, err) + assert.ErrorContains(t, err, tt.ExpectErr.Error()) + } else { + assert.NoError(t, err) + ApplyAssertions(t, authProvider, tt.Assertions) + } + }) + } +} + +func TestNewAuthProviderWithUserAssignedIdentity(t *testing.T) { + t.Parallel() + + var ( + testIdentityPath = "/var/run/identity.json" + testAzureAuthConfig = &AzureAuthConfig{ + AADMSIDataPlaneIdentityPath: testIdentityPath, + } + testCloudConfig = cloud.AzurePublic + testClientOption = &policy.ClientOptions{Cloud: testCloudConfig} + testFakeComputeTokenCredential = newFakeTokenCredential() + testErr = errors.New("test error") + ) + + tests := []struct { + Name string + AuthConfig *AzureAuthConfig + ClientOption *policy.ClientOptions + Opts *authProviderOptions + Assertions []AuthProviderAssertions + ExpectErr error + }{ + { + Name: "error when creating user assigned identity credential", + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewUserAssignedIdentityCredentialFn: func(_ context.Context, _ string, _ ...dataplane.Option) (azcore.TokenCredential, error) { + return nil, testErr + }, + }, + ExpectErr: testErr, + }, + { + Name: "success", + AuthConfig: testAzureAuthConfig, + ClientOption: testClientOption, + Opts: &authProviderOptions{ + NewUserAssignedIdentityCredentialFn: func(ctx context.Context, credentialPath string, opts ...dataplane.Option) (azcore.TokenCredential, error) { + assert.Equal(t, testIdentityPath, credentialPath) + // Check that the context is not nil + assert.NotNil(t, ctx) + // Verify we have at least one option passed (the cloud configuration) + assert.NotEmpty(t, opts) + return testFakeComputeTokenCredential, nil + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + authProvider, err := newAuthProviderWithUserAssignedIdentity( + tt.AuthConfig, + tt.ClientOption, + tt.Opts, + ) + + if tt.ExpectErr != nil { + assert.Error(t, err) + assert.ErrorIs(t, err, tt.ExpectErr) + } else { + assert.NoError(t, err) + ApplyAssertions(t, authProvider, tt.Assertions) + } + }) + } +} diff --git a/pkg/azclient/auth_option.go b/pkg/azclient/auth_option.go new file mode 100644 index 0000000000..2597d799c6 --- /dev/null +++ b/pkg/azclient/auth_option.go @@ -0,0 +1,150 @@ +/* +Copyright 2025 The Kubernetes 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 azclient + +import ( + "context" + "crypto" + "crypto/x509" + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/msi-dataplane/pkg/dataplane" + + "sigs.k8s.io/cloud-provider-azure/pkg/azclient/armauth" +) + +type ( + NewWorkloadIdentityCredentialFn func( + options *azidentity.WorkloadIdentityCredentialOptions, + ) (azcore.TokenCredential, error) + + NewManagedIdentityCredentialFn func( + options *azidentity.ManagedIdentityCredentialOptions, + ) (azcore.TokenCredential, error) + + NewClientSecretCredentialFn func( + tenantID string, + clientID string, + clientSecret string, + options *azidentity.ClientSecretCredentialOptions, + ) (azcore.TokenCredential, error) + + NewClientCertificateCredentialFn func( + tenantID string, + clientID string, + certs []*x509.Certificate, + key crypto.PrivateKey, + options *azidentity.ClientCertificateCredentialOptions, + ) (azcore.TokenCredential, error) + + NewKeyVaultCredentialFn func( + credential azcore.TokenCredential, + secretResourceID armauth.SecretResourceID, + ) (azcore.TokenCredential, error) + + NewUserAssignedIdentityCredentialFn func( + ctx context.Context, + credentialPath string, + opts ...dataplane.Option, + ) (azcore.TokenCredential, error) +) + +func DefaultNewWorkloadIdentityCredentialFn() NewWorkloadIdentityCredentialFn { + return func(options *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) { + return azidentity.NewWorkloadIdentityCredential(options) + } +} + +func DefaultNewManagedIdentityCredentialFn() NewManagedIdentityCredentialFn { + return func(options *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + return azidentity.NewManagedIdentityCredential(options) + } +} + +func DefaultNewClientSecretCredentialFn() NewClientSecretCredentialFn { + return func( + tenantID string, + clientID string, + clientSecret string, + options *azidentity.ClientSecretCredentialOptions, + ) (azcore.TokenCredential, error) { + return azidentity.NewClientSecretCredential(tenantID, clientID, clientSecret, options) + } +} +func DefaultNewClientCertificateCredentialFn() NewClientCertificateCredentialFn { + return func( + tenantID string, + clientID string, + certs []*x509.Certificate, + key crypto.PrivateKey, + options *azidentity.ClientCertificateCredentialOptions, + ) (azcore.TokenCredential, error) { + return azidentity.NewClientCertificateCredential(tenantID, clientID, certs, key, options) + } +} + +func DefaultNewKeyVaultCredentialFn() NewKeyVaultCredentialFn { + return func( + credential azcore.TokenCredential, + secretResourceID armauth.SecretResourceID, + ) (azcore.TokenCredential, error) { + return armauth.NewKeyVaultCredential(credential, secretResourceID) + } +} + +func DefaultNewUserAssignedIdentityCredentialFn() NewUserAssignedIdentityCredentialFn { + return dataplane.NewUserAssignedIdentityCredential +} + +type AuthProviderOption func(option *authProviderOptions) + +type authProviderOptions struct { + ClientOptionsMutFn []func(option *policy.ClientOptions) + // The following credential factory functions are for testing purposes only + // and should not be modified by users of this package + NewWorkloadIdentityCredentialFn NewWorkloadIdentityCredentialFn + NewManagedIdentityCredentialFn NewManagedIdentityCredentialFn + NewClientSecretCredentialFn NewClientSecretCredentialFn + NewClientCertificateCredentialFn NewClientCertificateCredentialFn + NewKeyVaultCredentialFn NewKeyVaultCredentialFn + NewUserAssignedIdentityCredentialFn NewUserAssignedIdentityCredentialFn + ReadFileFn func(name string) ([]byte, error) + ParseCertificatesFn func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) +} + +func defaultAuthProviderOptions() *authProviderOptions { + return &authProviderOptions{ + ClientOptionsMutFn: []func(option *policy.ClientOptions){}, + NewWorkloadIdentityCredentialFn: DefaultNewWorkloadIdentityCredentialFn(), + NewManagedIdentityCredentialFn: DefaultNewManagedIdentityCredentialFn(), + NewClientSecretCredentialFn: DefaultNewClientSecretCredentialFn(), + NewClientCertificateCredentialFn: DefaultNewClientCertificateCredentialFn(), + NewKeyVaultCredentialFn: DefaultNewKeyVaultCredentialFn(), + NewUserAssignedIdentityCredentialFn: DefaultNewUserAssignedIdentityCredentialFn(), + ReadFileFn: os.ReadFile, + ParseCertificatesFn: azidentity.ParseCertificates, + } +} + +func WithClientOptionsMutFn(fn func(option *policy.ClientOptions)) AuthProviderOption { + return func(option *authProviderOptions) { + option.ClientOptionsMutFn = append(option.ClientOptionsMutFn, fn) + } +} diff --git a/pkg/azclient/auth_option_test.go b/pkg/azclient/auth_option_test.go new file mode 100644 index 0000000000..8cee901e56 --- /dev/null +++ b/pkg/azclient/auth_option_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2025 The Kubernetes 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 azclient + +import ( + "os" + "reflect" + "runtime" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/msi-dataplane/pkg/dataplane" + "github.com/stretchr/testify/assert" +) + +func ExpectFuncEqual(t testing.TB, expected, actual any) { + t.Helper() + + exp := runtime.FuncForPC(reflect.ValueOf(expected).Pointer()).Name() + act := runtime.FuncForPC(reflect.ValueOf(actual).Pointer()).Name() + assert.Equal(t, exp, act) +} + +func TestDefaultAuthProviderOptions(t *testing.T) { + t.Parallel() + + opts := defaultAuthProviderOptions() + assert.NotNil(t, opts) + assert.NotNil(t, opts.NewWorkloadIdentityCredentialFn) + assert.NotNil(t, opts.NewManagedIdentityCredentialFn) + assert.NotNil(t, opts.NewClientSecretCredentialFn) + assert.NotNil(t, opts.NewClientCertificateCredentialFn) + assert.NotNil(t, opts.NewKeyVaultCredentialFn) + + assert.NotNil(t, opts.NewUserAssignedIdentityCredentialFn) + ExpectFuncEqual(t, dataplane.NewUserAssignedIdentityCredential, opts.NewUserAssignedIdentityCredentialFn) + + assert.NotNil(t, opts.ReadFileFn) + ExpectFuncEqual(t, os.ReadFile, opts.ReadFileFn) + + assert.NotNil(t, opts.ParseCertificatesFn) + ExpectFuncEqual(t, azidentity.ParseCertificates, opts.ParseCertificatesFn) +} diff --git a/pkg/azclient/auth_test.go b/pkg/azclient/auth_test.go new file mode 100644 index 0000000000..9155d6500e --- /dev/null +++ b/pkg/azclient/auth_test.go @@ -0,0 +1,374 @@ +/* +Copyright 2025 The Kubernetes 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 azclient + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/msi-dataplane/pkg/dataplane" + "github.com/go-faker/faker/v4" + "github.com/stretchr/testify/assert" +) + +func TestNewAuthProvider(t *testing.T) { + t.Parallel() + + var ( + testTenantID = faker.UUIDHyphenated() + testNetworkTenantID = faker.UUIDHyphenated() + testAADClientID = faker.UUIDHyphenated() + testAADClientSecret = faker.Password() + testAADClientCertPath = faker.Word() + testAADClientCertPassword = faker.Password() + testIdentityPath = faker.Word() + testFederatedTokenPath = faker.Word() + testCloudConfig = cloud.AzurePublic + testFakeComputeTokenCredential = newFakeTokenCredential() + testFakeNetworkTokenCredential = newFakeTokenCredential() + testErr = errors.New("test error") + ) + + testARMConfig := &ARMClientConfig{ + TenantID: testTenantID, + } + + testARMConfigMultiTenant := &ARMClientConfig{ + TenantID: testTenantID, + NetworkResourceTenantID: testNetworkTenantID, + } + + tests := []struct { + Name string + ARMConfig *ARMClientConfig + AuthConfig *AzureAuthConfig + Options []AuthProviderOption + Assertions []AuthProviderAssertions + ExpectErr error + ErrorContains string + }{ + { + Name: "error when GetAzCoreClientOption fails", + ARMConfig: &ARMClientConfig{}, // This will cause validation failure + AuthConfig: &AzureAuthConfig{}, + ExpectErr: ErrNoValidAuthMethodFound, // The error won't be this, but we expect an error + ErrorContains: "invalid ARM client config", + }, + { + Name: "error when no valid auth method found", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{}, + ExpectErr: ErrNoValidAuthMethodFound, + }, + { + Name: "success with workload identity", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + AADClientID: testAADClientID, + AADFederatedTokenFile: testFederatedTokenPath, + UseFederatedWorkloadIdentityExtension: true, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewWorkloadIdentityCredentialFn = func(_ *azidentity.WorkloadIdentityCredentialOptions) (azcore.TokenCredential, error) { + return testFakeComputeTokenCredential, nil + } + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "success with managed identity", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + UseManagedIdentityExtension: true, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewManagedIdentityCredentialFn = func(_ *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + return testFakeComputeTokenCredential, nil + } + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "error with managed identity", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + UseManagedIdentityExtension: true, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewManagedIdentityCredentialFn = func(_ *azidentity.ManagedIdentityCredentialOptions) (azcore.TokenCredential, error) { + return nil, testErr + } + }, + }, + ExpectErr: testErr, + }, + { + Name: "success with service principal client secret", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + AADClientID: testAADClientID, + AADClientSecret: testAADClientSecret, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewClientSecretCredentialFn = func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, testCloudConfig, options.ClientOptions.Cloud) + return testFakeComputeTokenCredential, nil + } + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "success with service principal client certificate", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + AADClientID: testAADClientID, + AADClientCertPath: testAADClientCertPath, + AADClientCertPassword: testAADClientCertPassword, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + testCertData := []byte(faker.Word()) + option.ReadFileFn = func(name string) ([]byte, error) { + assert.Equal(t, testAADClientCertPath, name) + return testCertData, nil + } + option.ParseCertificatesFn = func(certData []byte, password []byte) ([]*x509.Certificate, crypto.PrivateKey, error) { + assert.Equal(t, testCertData, certData) + assert.Equal(t, []byte(testAADClientCertPassword), password) + return []*x509.Certificate{{}}, struct{ crypto.PrivateKey }{}, nil + } + option.NewClientCertificateCredentialFn = func(tenantID, clientID string, _ []*x509.Certificate, _ crypto.PrivateKey, options *azidentity.ClientCertificateCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testCloudConfig, options.ClientOptions.Cloud) + return testFakeComputeTokenCredential, nil + } + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "success with user assigned identity", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + AADMSIDataPlaneIdentityPath: testIdentityPath, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewUserAssignedIdentityCredentialFn = func(_ context.Context, credentialPath string, _ ...dataplane.Option) (azcore.TokenCredential, error) { + assert.Equal(t, testIdentityPath, credentialPath) + return testFakeComputeTokenCredential, nil + } + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "success with client option mutation", + ARMConfig: testARMConfig, + AuthConfig: &AzureAuthConfig{ + AADClientID: testAADClientID, + AADClientSecret: testAADClientSecret, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewClientSecretCredentialFn = func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, testCloudConfig, options.ClientOptions.Cloud) + return testFakeComputeTokenCredential, nil + } + }, + WithClientOptionsMutFn(func(option *policy.ClientOptions) { + // Just to test that the mutation function is called + option.Retry.MaxRetries = 5 + }), + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNilNetworkTokenCredential(), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + { + Name: "success with multi-tenant service principal", + ARMConfig: testARMConfigMultiTenant, + AuthConfig: &AzureAuthConfig{ + AADClientID: testAADClientID, + AADClientSecret: testAADClientSecret, + }, + Options: []AuthProviderOption{ + func(option *authProviderOptions) { + option.NewClientSecretCredentialFn = func(tenantID, clientID, clientSecret string, options *azidentity.ClientSecretCredentialOptions) (azcore.TokenCredential, error) { + if tenantID == testNetworkTenantID { + assert.Equal(t, testNetworkTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, testCloudConfig, options.ClientOptions.Cloud) + return testFakeNetworkTokenCredential, nil + } + + assert.Equal(t, testTenantID, tenantID) + assert.Equal(t, testAADClientID, clientID) + assert.Equal(t, testAADClientSecret, clientSecret) + assert.Equal(t, testCloudConfig, options.ClientOptions.Cloud) + return testFakeComputeTokenCredential, nil + } + }, + }, + Assertions: []AuthProviderAssertions{ + AssertComputeTokenCredential(testFakeComputeTokenCredential), + AssertNetworkTokenCredential(testFakeNetworkTokenCredential), + AssertEmptyAdditionalComputeClientOptions(), + AssertCloudConfig(testCloudConfig), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + + authProvider, err := NewAuthProvider( + tt.ARMConfig, + tt.AuthConfig, + tt.Options..., + ) + + if tt.ExpectErr != nil { + assert.Error(t, err) + if errors.Is(err, tt.ExpectErr) { + assert.ErrorIs(t, err, tt.ExpectErr) + } else if tt.ErrorContains != "" { + assert.ErrorContains(t, err, tt.ErrorContains) + } + return + } + + assert.NoError(t, err) + ApplyAssertions(t, authProvider, tt.Assertions) + + // Test additional methods + assert.Equal(t, authProvider.ComputeCredential, authProvider.GetAzIdentity()) + + networkIdentity := authProvider.GetNetworkAzIdentity() + if authProvider.NetworkCredential != nil { + assert.Equal(t, authProvider.NetworkCredential, networkIdentity) + } else { + assert.Equal(t, authProvider.ComputeCredential, networkIdentity) + } + + expectedScope := DefaultTokenScopeFor(testCloudConfig) + assert.Equal(t, expectedScope, authProvider.DefaultTokenScope()) + }) + } +} + +func TestDefaultTokenScopeFor(t *testing.T) { + t.Parallel() + + tests := []struct { + Name string + CloudCfg cloud.Configuration + Expected string + }{ + { + Name: "AzurePublic", + CloudCfg: cloud.AzurePublic, + Expected: "https://management.core.windows.net/.default", + }, + { + Name: "AzureChina", + CloudCfg: cloud.AzureChina, + Expected: "https://management.core.chinacloudapi.cn/.default", + }, + { + Name: "Custom audience with trailing slash", + CloudCfg: cloud.Configuration{ + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{ + cloud.ResourceManager: { + Audience: "https://custom.endpoint.com/", + }, + }, + }, + Expected: "https://custom.endpoint.com/.default", + }, + { + Name: "Custom audience without trailing slash", + CloudCfg: cloud.Configuration{ + Services: map[cloud.ServiceName]cloud.ServiceConfiguration{ + cloud.ResourceManager: { + Audience: "https://custom.endpoint.com", + }, + }, + }, + Expected: "https://custom.endpoint.com/.default", + }, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + got := DefaultTokenScopeFor(tt.CloudCfg) + assert.Equal(t, tt.Expected, got) + }) + } +} diff --git a/pkg/azclient/go.mod b/pkg/azclient/go.mod index beeea9ec2f..cea77874ab 100644 --- a/pkg/azclient/go.mod +++ b/pkg/azclient/go.mod @@ -19,9 +19,11 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.7.0 github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 github.com/Azure/msi-dataplane v0.4.3 + github.com/go-faker/faker/v4 v4.6.0 github.com/google/uuid v1.6.0 github.com/onsi/ginkgo/v2 v2.23.0 github.com/onsi/gomega v1.36.2 + github.com/stretchr/testify v1.10.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/metric v1.35.0 go.uber.org/mock v0.5.0 @@ -38,6 +40,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect @@ -46,6 +49,7 @@ require ( github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/sys v0.31.0 // indirect diff --git a/pkg/azclient/go.sum b/pkg/azclient/go.sum index c1e7d3921c..aa4b09a75b 100644 --- a/pkg/azclient/go.sum +++ b/pkg/azclient/go.sum @@ -54,6 +54,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs= +github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=