diff --git a/CHANGELOG.md b/CHANGELOG.md index 75d736dc449..7fb5f8286e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ Here is an overview of all new **experimental** features: ### Fixes - **General**: Prevent a panic that might occur while refreshing a scaler cache ([#4092](https://github.com/kedacore/keda/issues/4092)) +- **Azure Service Bus Scaler:** Use correct auth flows with pod identity ([#4026](https://github.com/kedacore/keda/issues/4026)) - **CPU Memory Scaler** Store forgotten logger ([#4022](https://github.com/kedacore/keda/issues/4022)) ### Deprecations diff --git a/Makefile b/Makefile index fbe0b566662..bc48f92a581 100644 --- a/Makefile +++ b/Makefile @@ -244,9 +244,15 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && \ - $(KUSTOMIZE) edit set image ghcr.io/kedacore/keda=${IMAGE_CONTROLLER} + $(KUSTOMIZE) edit set image ghcr.io/kedacore/keda=${IMAGE_CONTROLLER} && \ + if [ "$(AZURE_RUN_AAD_POD_IDENTITY_TESTS)" = true ]; then \ + $(KUSTOMIZE) edit add label --force aadpodidbinding:keda; \ + fi cd config/metrics-server && \ - $(KUSTOMIZE) edit set image ghcr.io/kedacore/keda-metrics-apiserver=${IMAGE_ADAPTER} + $(KUSTOMIZE) edit set image ghcr.io/kedacore/keda-metrics-apiserver=${IMAGE_ADAPTER} && \ + if [ "$(AZURE_RUN_AAD_POD_IDENTITY_TESTS)" = true ]; then \ + $(KUSTOMIZE) edit add label --force aadpodidbinding:keda; \ + fi if [ "$(AZURE_RUN_WORKLOAD_IDENTITY_TESTS)" = true ]; then \ cd config/service_account && \ $(KUSTOMIZE) edit add label --force azure.workload.identity/use:true; \ diff --git a/go.mod b/go.mod index f7fd212253f..2e63d22fe56 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/Azure/azure-kusto-go v0.9.2 github.com/Azure/azure-sdk-for-go v67.1.0+incompatible github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.2 github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.3 github.com/Azure/azure-storage-blob-go v0.15.0 github.com/Azure/azure-storage-queue-go v0.0.0-20191125232315-636801874cdd diff --git a/go.sum b/go.sum index 2c2cfcb28b8..aa39aef7152 100644 --- a/go.sum +++ b/go.sum @@ -66,8 +66,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0 h1:sVW/AFBTGyJxDaMYlq0ct3jUXTtj12tQ6zE2GZUgVQw= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.2.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 h1:t/W5MYAuQy81cvM8VUNfRLzhtKpXhVUAN7Cd7KVbTyc= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.2 h1:NsprcuNHEsCR48QYlLxx/gAi9OcCzcwX8VVTZVK8fdQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.2/go.mod h1:NBanQUfSWiWn3QEpWDTCU0IjBECKOYvl2R8xdRtMtiM= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 h1:XUNQ4mw+zJmaA2KXzP9JlQiecy1SI+Eog7xVkPiqIbg= diff --git a/pkg/metricsservice/utils/tls.go b/pkg/metricsservice/utils/tls.go index c8418a506a7..3cc72ddf159 100644 --- a/pkg/metricsservice/utils/tls.go +++ b/pkg/metricsservice/utils/tls.go @@ -20,7 +20,7 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "io/ioutil" + "os" "path" "google.golang.org/grpc/credentials" @@ -29,7 +29,7 @@ import ( // LoadGrpcTLSCredentials reads the certificate from the given path and returns TLS transport credentials func LoadGrpcTLSCredentials(certDir string, server bool) (credentials.TransportCredentials, error) { // Load certificate of the CA who signed client's certificate - pemClientCA, err := ioutil.ReadFile(path.Join(certDir, "ca.crt")) + pemClientCA, err := os.ReadFile(path.Join(certDir, "ca.crt")) if err != nil { return nil, err } diff --git a/pkg/scalers/azure/azure_aad_podidentity.go b/pkg/scalers/azure/azure_aad_podidentity.go index 8eb6a5e81cd..90307f693e7 100644 --- a/pkg/scalers/azure/azure_aad_podidentity.go +++ b/pkg/scalers/azure/azure_aad_podidentity.go @@ -8,6 +8,13 @@ import ( "io" "net/http" "net/url" + "os" + "strconv" + "time" + + "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/kedacore/keda/v2/pkg/util" ) @@ -17,6 +24,20 @@ const ( MSIURLWithClientID = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=%s&client_id=%s" ) +var globalHTTPTimeout time.Duration + +func init() { + valueStr, found := os.LookupEnv("KEDA_HTTP_DEFAULT_TIMEOUT") + globalHTTPTimeoutMS := 3000 + if found && valueStr != "" { + value, err := strconv.Atoi(valueStr) + if err == nil { + globalHTTPTimeoutMS = value + } + } + globalHTTPTimeout = time.Duration(globalHTTPTimeoutMS) * time.Millisecond +} + // GetAzureADPodIdentityToken returns the AADToken for resource func GetAzureADPodIdentityToken(ctx context.Context, httpClient util.HTTPDoer, identityID, audience string) (AADToken, error) { var token AADToken @@ -54,3 +75,33 @@ func GetAzureADPodIdentityToken(ctx context.Context, httpClient util.HTTPDoer, i return token, nil } + +type ManagedIdentityWrapper struct { + cred *azidentity.ManagedIdentityCredential +} + +func ManagedIdentityWrapperCredential(clientID string) (*ManagedIdentityWrapper, error) { + opts := &azidentity.ManagedIdentityCredentialOptions{} + if clientID != "" { + opts.ID = azidentity.ClientID(clientID) + } + + msiCred, err := azidentity.NewManagedIdentityCredential(opts) + if err != nil { + return nil, err + } + return &ManagedIdentityWrapper{ + cred: msiCred, + }, nil +} + +func (w *ManagedIdentityWrapper) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { + c, cancel := context.WithTimeout(ctx, globalHTTPTimeout) + defer cancel() + tk, err := w.cred.GetToken(c, opts) + if ctxErr := c.Err(); errors.Is(ctxErr, context.DeadlineExceeded) { + // timeout: signal the chain to try its next credential, if any + err = azidentity.NewCredentialUnavailableError("managed identity timed out") + } + return tk, err +} diff --git a/pkg/scalers/azure/azure_aad_workload_identity.go b/pkg/scalers/azure/azure_aad_workload_identity.go index 124b79d55a3..610aed9ab91 100644 --- a/pkg/scalers/azure/azure_aad_workload_identity.go +++ b/pkg/scalers/azure/azure_aad_workload_identity.go @@ -25,8 +25,7 @@ import ( "time" amqpAuth "github.com/Azure/azure-amqp-common-go/v3/auth" - "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/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" @@ -45,18 +44,26 @@ const ( azureAuthrityHostEnv = "AZURE_AUTHORITY_HOST" ) +var DefaultClientID string +var TenantID string +var TokenFilePath string +var AuthorityHost string + +func init() { + DefaultClientID = os.Getenv(azureClientIDEnv) + TenantID = os.Getenv(azureTenantIDEnv) + TokenFilePath = os.Getenv(azureFederatedTokenFileEnv) + AuthorityHost = os.Getenv(azureAuthrityHostEnv) +} + // GetAzureADWorkloadIdentityToken returns the AADToken for resource func GetAzureADWorkloadIdentityToken(ctx context.Context, identityID, resource string) (AADToken, error) { - clientID := os.Getenv(azureClientIDEnv) - tenantID := os.Getenv(azureTenantIDEnv) - tokenFilePath := os.Getenv(azureFederatedTokenFileEnv) - authorityHost := os.Getenv(azureAuthrityHostEnv) - + clientID := DefaultClientID if identityID != "" { clientID = identityID } - signedAssertion, err := readJWTFromFileSystem(tokenFilePath) + signedAssertion, err := readJWTFromFileSystem(TokenFilePath) if err != nil { return AADToken{}, fmt.Errorf("error reading service account token - %w", err) } @@ -69,7 +76,7 @@ func GetAzureADWorkloadIdentityToken(ctx context.Context, identityID, resource s return signedAssertion, nil }) - authorityOption := confidential.WithAuthority(fmt.Sprintf("%s%s/oauth2/token", authorityHost, tenantID)) + authorityOption := confidential.WithAuthority(fmt.Sprintf("%s%s/oauth2/token", AuthorityHost, TenantID)) confidentialClient, err := confidential.New( clientID, cred, @@ -126,46 +133,12 @@ func (aadWiConfig ADWorkloadIdentityConfig) Authorizer() (autorest.Authorizer, e aadWiConfig.ctx, aadWiConfig.IdentityID, aadWiConfig.Resource)), nil } -// ADWorkloadIdentityCredential is a type that implements the TokenCredential interface. -// Once azure-sdk-for-go supports Workload Identity we can remove this and use default implementation -// https://github.com/Azure/azure-sdk-for-go/issues/15615 -type ADWorkloadIdentityCredential struct { - ctx context.Context - IdentityID string - Resource string - aadToken AADToken -} - -func NewADWorkloadIdentityCredential(ctx context.Context, identityID, resource string) *ADWorkloadIdentityCredential { - return &ADWorkloadIdentityCredential{ctx: ctx, IdentityID: identityID, Resource: resource} -} - -func (wiCredential *ADWorkloadIdentityCredential) refresh() error { - if time.Now().Before(wiCredential.aadToken.ExpiresOnTimeObject) { - return nil - } - - aadToken, err := GetAzureADWorkloadIdentityToken(wiCredential.ctx, wiCredential.IdentityID, wiCredential.Resource) - if err != nil { - return err - } - - wiCredential.aadToken = aadToken - return nil -} - -// GetToken is for implementing the TokenCredential interface -func (wiCredential *ADWorkloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { - accessToken := azcore.AccessToken{} - err := wiCredential.refresh() - if err != nil { - return accessToken, err +func NewADWorkloadIdentityCredential(identityID string) (*azidentity.WorkloadIdentityCredential, error) { + clientID := DefaultClientID + if identityID != "" { + clientID = identityID } - - accessToken.Token = wiCredential.aadToken.AccessToken - accessToken.ExpiresOn = wiCredential.aadToken.ExpiresOnTimeObject - - return accessToken, nil + return azidentity.NewWorkloadIdentityCredential(TenantID, clientID, TokenFilePath, &azidentity.WorkloadIdentityCredentialOptions{}) } // ADWorkloadIdentityTokenProvider is a type that implements the adal.OAuthTokenProvider and adal.Refresher interfaces. diff --git a/pkg/scalers/azure/azure_azidentity_chain.go b/pkg/scalers/azure/azure_azidentity_chain.go new file mode 100644 index 00000000000..df8b38381b8 --- /dev/null +++ b/pkg/scalers/azure/azure_azidentity_chain.go @@ -0,0 +1,35 @@ +package azure + +import ( + "os" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" +) + +func NewChainedCredential(identityID string) (*azidentity.ChainedTokenCredential, error) { + var creds []azcore.TokenCredential + + // Used for local debug based on az-cli user + // As production images don't have shell, we can't register this provider always + if _, err := os.Stat("/bin/sh"); err == nil { + cliCred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{}) + if err == nil { + creds = append(creds, cliCred) + } + } + + // Used for aad-pod-identity + msiCred, err := ManagedIdentityWrapperCredential(identityID) + if err == nil { + creds = append(creds, msiCred) + } + + wiCred, err := NewADWorkloadIdentityCredential(identityID) + if err == nil { + creds = append(creds, wiCred) + } + + // Create the chained credential based on the previous 3 + return azidentity.NewChainedTokenCredential(creds, nil) +} diff --git a/pkg/scalers/azure_servicebus_scaler.go b/pkg/scalers/azure_servicebus_scaler.go index b9aba816549..ebb3ff7f5ad 100755 --- a/pkg/scalers/azure_servicebus_scaler.go +++ b/pkg/scalers/azure_servicebus_scaler.go @@ -22,8 +22,6 @@ import ( "regexp" "strconv" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" az "github.com/Azure/go-autorest/autorest/azure" "github.com/go-logr/logr" @@ -44,8 +42,6 @@ const ( messageCountMetricName = "messageCount" activationMessageCountMetricName = "activationMessageCount" defaultTargetMessageCount = 5 - // Service bus resource id is "https://servicebus.azure.net/" in all cloud environments - serviceBusResource = "https://servicebus.azure.net/" ) type azureServiceBusScaler struct { @@ -275,7 +271,7 @@ func (s *azureServiceBusScaler) GetMetricsAndActivity(ctx context.Context, metri // Returns the length of the queue or subscription func (s *azureServiceBusScaler) getAzureServiceBusLength(ctx context.Context) (int64, error) { // get adminClient - adminClient, err := s.getServiceBusAdminClient(ctx) + adminClient, err := s.getServiceBusAdminClient() if err != nil { return -1, err } @@ -291,58 +287,22 @@ func (s *azureServiceBusScaler) getAzureServiceBusLength(ctx context.Context) (i } // Returns service bus namespace object -func (s *azureServiceBusScaler) getServiceBusAdminClient(ctx context.Context) (*admin.Client, error) { +func (s *azureServiceBusScaler) getServiceBusAdminClient() (*admin.Client, error) { if s.client != nil { return s.client, nil } - var adminClient *admin.Client - var err error - switch s.podIdentity.Provider { case "", kedav1alpha1.PodIdentityProviderNone: - adminClient, err = admin.NewClientFromConnectionString(s.metadata.connection, nil) - if err != nil { - return nil, err - } - + return admin.NewClientFromConnectionString(s.metadata.connection, nil) case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload: - var creds []azcore.TokenCredential - options := &azidentity.DefaultAzureCredentialOptions{} - - // Used for local debug based on az-cli user - cliCred, err := azidentity.NewAzureCLICredential(&azidentity.AzureCLICredentialOptions{TenantID: options.TenantID}) - if err == nil { - creds = append(creds, cliCred) - } - - // Once azure-sdk-for-go supports Workload Identity we can remove this and use default implementation - // https://github.com/Azure/azure-sdk-for-go/issues/15615 - wiCred := azure.NewADWorkloadIdentityCredential(ctx, s.podIdentity.IdentityID, serviceBusResource) - creds = append(creds, wiCred) - - // Used for aad-pod-identity - o := &azidentity.ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions} - if s.podIdentity.IdentityID != "" { - o.ID = azidentity.ClientID(s.podIdentity.IdentityID) - } - msiCred, err := azidentity.NewManagedIdentityCredential(o) - if err == nil { - creds = append(creds, msiCred) - } - - // Create the chained credential based on the previous 3 - chain, err := azidentity.NewChainedTokenCredential(creds, nil) - if err != nil { - return nil, err - } - adminClient, err = admin.NewClient(s.metadata.fullyQualifiedNamespace, chain, nil) + creds, err := azure.NewChainedCredential(s.podIdentity.IdentityID) if err != nil { return nil, err } + return admin.NewClient(s.metadata.fullyQualifiedNamespace, creds, nil) } - - return adminClient, nil + return nil, fmt.Errorf("incorrect podIdentity type") } func getQueueLength(ctx context.Context, adminClient *admin.Client, meta *azureServiceBusMetadata) (int64, error) { diff --git a/tests/helper/helper.go b/tests/helper/helper.go index 9163db77a94..7f0daa4766b 100644 --- a/tests/helper/helper.go +++ b/tests/helper/helper.go @@ -33,6 +33,7 @@ import ( ) const ( + AzureAdPodIdentityNamespace = "azure-ad-identity-system" AzureWorkloadIdentityNamespace = "azure-workload-identity-system" AwsIdentityNamespace = "aws-identity-system" GcpIdentityNamespace = "gcp-identity-system" @@ -54,7 +55,10 @@ var random = rand.New(rand.NewSource(time.Now().UnixNano())) // Env variables required for setup and cleanup. var ( + AzureADMsiID = os.Getenv("TF_AZURE_IDENTITY_1_APP_FULL_ID") + AzureADMsiClientID = os.Getenv("TF_AZURE_IDENTITY_1_APP_ID") AzureADTenantID = os.Getenv("TF_AZURE_SP_TENANT") + AzureRunAadPodIdentityTests = os.Getenv("AZURE_RUN_AAD_POD_IDENTITY_TESTS") AzureRunWorkloadIdentityTests = os.Getenv("AZURE_RUN_WORKLOAD_IDENTITY_TESTS") AwsIdentityTests = os.Getenv("AWS_RUN_IDENTITY_TESTS") GcpIdentityTests = os.Getenv("GCP_RUN_IDENTITY_TESTS") diff --git a/tests/scalers/azure/azure_service_bus_queue_aad_pod_identity/azure_service_bus_queue_aad_pod_identity_test.go b/tests/scalers/azure/azure_service_bus_queue_aad_pod_identity/azure_service_bus_queue_aad_pod_identity_test.go new file mode 100644 index 00000000000..a085972e9c9 --- /dev/null +++ b/tests/scalers/azure/azure_service_bus_queue_aad_pod_identity/azure_service_bus_queue_aad_pod_identity_test.go @@ -0,0 +1,212 @@ +//go:build e2e +// +build e2e + +package azure_service_bus_queue_aad_pod_identity_test + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "azure-service-bus-queue-aad-pod-identity-test" +) + +var ( + connectionString = os.Getenv("TF_AZURE_SERVICE_BUS_CONNECTION_STRING") + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + triggerAuthName = fmt.Sprintf("%s-ta", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + queueName = fmt.Sprintf("queue-%d", GetRandomNumber()) +) + +type templateData struct { + TestNamespace string + Connection string + DeploymentName string + TriggerAuthName string + ScaledObjectName string + ServiceBusNamespace string + QueueName string +} + +const ( + deploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: nginx + image: nginx:1.16.1 +` + + triggerAuthTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure +` + + scaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + deploymentName: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 1 + triggers: + - type: azure-servicebus + metadata: + queueName: {{.QueueName}} + activationMessageCount: "5" + namespace: {{.ServiceBusNamespace}} + authenticationRef: + name: {{.TriggerAuthName}} +` +) + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + require.NotEmpty(t, connectionString, "TF_AZURE_SERVICE_BUS_CONNECTION_STRING env variable is required for service bus tests") + + client, adminClient, namespace := setupServiceBusQueue(t) + + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + data.ServiceBusNamespace = namespace + + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") + + // test scaling + testActivation(t, kc, client) + testScaleOut(t, kc, client) + testScaleIn(t, kc, adminClient) + + // cleanup + DeleteKubernetesResources(t, kc, testNamespace, data, templates) + cleanupServiceBusQueue(t, adminClient) +} + +func setupServiceBusQueue(t *testing.T) (*azservicebus.Client, *admin.Client, string) { + adminClient, err := admin.NewClientFromConnectionString(connectionString, nil) + assert.NoErrorf(t, err, "cannot connect to service bus namespace - %s", err) + + // Delete the queue if already exists + _, _ = adminClient.DeleteQueue(context.Background(), queueName, nil) + + _, err = adminClient.CreateQueue(context.Background(), queueName, nil) + assert.NoErrorf(t, err, "cannot create the queue - %s", err) + + namespace, err := adminClient.GetNamespaceProperties(context.Background(), nil) + assert.NoErrorf(t, err, "cannot get namespace info - %s", err) + + client, err := azservicebus.NewClientFromConnectionString(connectionString, nil) + assert.NoErrorf(t, err, "cannot connect to service bus namespace - %s", err) + + return client, adminClient, namespace.Name +} + +func getTemplateData() (templateData, []Template) { + base64ConnectionString := base64.StdEncoding.EncodeToString([]byte(connectionString)) + + return templateData{ + TestNamespace: testNamespace, + Connection: base64ConnectionString, + DeploymentName: deploymentName, + TriggerAuthName: triggerAuthName, + ScaledObjectName: scaledObjectName, + QueueName: queueName, + }, []Template{ + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "triggerAuthTemplate", Config: triggerAuthTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +} + +func testActivation(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Client) { + t.Log("--- testing activation ---") + addMessages(t, client, 3) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 60) +} + +func testScaleOut(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Client) { + t.Log("--- testing scale out ---") + addMessages(t, client, 10) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 1, 60, 1), + "replica count should be 1 after 1 minute") +} + +func testScaleIn(t *testing.T, kc *kubernetes.Clientset, adminClient *admin.Client) { + t.Log("--- testing scale in ---") + + _, err := adminClient.DeleteQueue(context.Background(), queueName, nil) + assert.NoErrorf(t, err, "cannot delete the queue - %s", err) + _, err = adminClient.CreateQueue(context.Background(), queueName, nil) + assert.NoErrorf(t, err, "cannot create the queue - %s", err) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func addMessages(t *testing.T, client *azservicebus.Client, count int) { + sender, err := client.NewSender(queueName, nil) + assert.NoErrorf(t, err, "cannot create the sender - %s", err) + for i := 0; i < count; i++ { + msg := fmt.Sprintf("Message - %d", i) + _ = sender.SendMessage(context.Background(), &azservicebus.Message{ + Body: []byte(msg), + }, nil) + } +} + +func cleanupServiceBusQueue(t *testing.T, adminClient *admin.Client) { + t.Log("--- cleaning up ---") + _, err := adminClient.DeleteQueue(context.Background(), queueName, nil) + assert.NoErrorf(t, err, "cannot delete service bus queue - %s", err) +} diff --git a/tests/secret-providers/azure_workload_identity/azure_workload_identity_test.go b/tests/scalers/azure/azure_service_bus_queue_aad_wi/azure_service_bus_queue_aad_wi_test.go similarity index 97% rename from tests/secret-providers/azure_workload_identity/azure_workload_identity_test.go rename to tests/scalers/azure/azure_service_bus_queue_aad_wi/azure_service_bus_queue_aad_wi_test.go index 98d1d202930..3b17da8a99b 100644 --- a/tests/secret-providers/azure_workload_identity/azure_workload_identity_test.go +++ b/tests/scalers/azure/azure_service_bus_queue_aad_wi/azure_service_bus_queue_aad_wi_test.go @@ -1,7 +1,7 @@ //go:build e2e // +build e2e -package azure_workload_identity_test +package azure_service_bus_queue_aad_pod_identity_test import ( "context" @@ -20,10 +20,10 @@ import ( ) // Load environment variables from .env file -var _ = godotenv.Load("../../.env") +var _ = godotenv.Load("../../../.env") const ( - testName = "azure-workload-identity-test" + testName = "azure-service-bus-queue-aad-wi-test" ) var ( diff --git a/tests/scalers/azure/azure_service_bus_topic_aad_pod_identity/azure_service_bus_topic_aad_pod_identity_test.go b/tests/scalers/azure/azure_service_bus_topic_aad_pod_identity/azure_service_bus_topic_aad_pod_identity_test.go new file mode 100644 index 00000000000..706a6386d12 --- /dev/null +++ b/tests/scalers/azure/azure_service_bus_topic_aad_pod_identity/azure_service_bus_topic_aad_pod_identity_test.go @@ -0,0 +1,220 @@ +//go:build e2e +// +build e2e + +package azure_service_bus_topic_aad_pod_identity_test + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "azure-service-bus-topic-aad-pod-identity-test" +) + +var ( + connectionString = os.Getenv("TF_AZURE_SERVICE_BUS_CONNECTION_STRING") + testNamespace = fmt.Sprintf("%s-ns", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + triggerAuthName = fmt.Sprintf("%s-ta", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + topicName = fmt.Sprintf("topic-%d", GetRandomNumber()) + subscriptionName = fmt.Sprintf("subs-%d", GetRandomNumber()) +) + +type templateData struct { + TestNamespace string + Connection string + DeploymentName string + TriggerAuthName string + ScaledObjectName string + TopicName string + SubscriptionName string + ServiceBusNamespace string +} + +const ( + deploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: nginx + image: nginx:1.16.1 +` + + triggerAuthTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure +` + + scaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + deploymentName: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 1 + triggers: + - type: azure-servicebus + metadata: + topicName: {{.TopicName}} + subscriptionName: {{.SubscriptionName}} + namespace: {{.ServiceBusNamespace}} + activationMessageCount: "5" + authenticationRef: + name: {{.TriggerAuthName}} +` +) + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + require.NotEmpty(t, connectionString, "TF_AZURE_SERVICE_BUS_CONNECTION_STRING env variable is required for service bus tests") + + client, adminClient, namespace := setupServiceBusTopicAndSubscription(t) + + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + data.ServiceBusNamespace = namespace + + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") + + // test scaling + testActivation(t, kc, client) + testScaleOut(t, kc, client) + testScaleIn(t, kc, adminClient) + + // cleanup + DeleteKubernetesResources(t, kc, testNamespace, data, templates) + cleanupServiceBusTopic(t, adminClient) +} + +func setupServiceBusTopicAndSubscription(t *testing.T) (*azservicebus.Client, *admin.Client, string) { + adminClient, err := admin.NewClientFromConnectionString(connectionString, nil) + assert.NoErrorf(t, err, "cannot connect to service bus namespace - %s", err) + + // Delete the topic if already exists + _, _ = adminClient.DeleteTopic(context.Background(), topicName, nil) + + _, err = adminClient.CreateTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot create the topic - %s", err) + _, err = adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil) + assert.NoErrorf(t, err, "cannot create the subscription - %s", err) + + namespace, err := adminClient.GetNamespaceProperties(context.Background(), nil) + assert.NoErrorf(t, err, "cannot get namespace info - %s", err) + + client, err := azservicebus.NewClientFromConnectionString(connectionString, nil) + assert.NoErrorf(t, err, "cannot connect to service bus namespace - %s", err) + + return client, adminClient, namespace.Name +} + +func getTemplateData() (templateData, []Template) { + base64ConnectionString := base64.StdEncoding.EncodeToString([]byte(connectionString)) + + return templateData{ + TestNamespace: testNamespace, + Connection: base64ConnectionString, + DeploymentName: deploymentName, + TriggerAuthName: triggerAuthName, + ScaledObjectName: scaledObjectName, + TopicName: topicName, + SubscriptionName: subscriptionName, + }, []Template{ + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "triggerAuthTemplate", Config: triggerAuthTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +} + +func testActivation(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Client) { + t.Log("--- testing activation ---") + addMessages(t, client, 4) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 60) +} + +func testScaleOut(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Client) { + t.Log("--- testing scale out ---") + addMessages(t, client, 10) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 1, 60, 1), + "replica count should be 1 after 1 minute") +} + +func testScaleIn(t *testing.T, kc *kubernetes.Clientset, adminClient *admin.Client) { + t.Log("--- testing scale in ---") + + _, err := adminClient.DeleteTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot delete the topic - %s", err) + _, err = adminClient.CreateTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot create the topic - %s", err) + _, err = adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil) + assert.NoErrorf(t, err, "cannot create the subscription - %s", err) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func addMessages(t *testing.T, client *azservicebus.Client, count int) { + sender, err := client.NewSender(topicName, nil) + assert.NoErrorf(t, err, "cannot create the sender - %s", err) + for i := 0; i < count; i++ { + msg := fmt.Sprintf("Message - %d", i) + _ = sender.SendMessage(context.Background(), &azservicebus.Message{ + Body: []byte(msg), + }, nil) + } +} + +func cleanupServiceBusTopic(t *testing.T, adminClient *admin.Client) { + t.Log("--- cleaning up ---") + _, err := adminClient.DeleteTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot delete service bus topic - %s", err) +} diff --git a/tests/scalers/azure/azure_service_bus_topic_aad_wi/azure_service_bus_topic_aad_wi_test.go b/tests/scalers/azure/azure_service_bus_topic_aad_wi/azure_service_bus_topic_aad_wi_test.go new file mode 100644 index 00000000000..710cacc2f6a --- /dev/null +++ b/tests/scalers/azure/azure_service_bus_topic_aad_wi/azure_service_bus_topic_aad_wi_test.go @@ -0,0 +1,235 @@ +//go:build e2e +// +build e2e + +package azure_service_bus_topic_aad_wi_test + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/kubernetes" + + . "github.com/kedacore/keda/v2/tests/helper" +) + +// Load environment variables from .env file +var _ = godotenv.Load("../../../.env") + +const ( + testName = "azure-service-bus-topic-aad-wi-test" +) + +var ( + connectionString = os.Getenv("TF_AZURE_SERVICE_BUS_CONNECTION_STRING") + testNamespace = fmt.Sprintf("%s-ns", testName) + secretName = fmt.Sprintf("%s-secret", testName) + deploymentName = fmt.Sprintf("%s-deployment", testName) + triggerAuthName = fmt.Sprintf("%s-ta", testName) + scaledObjectName = fmt.Sprintf("%s-so", testName) + topicName = fmt.Sprintf("topic-%d", GetRandomNumber()) + subscriptionName = fmt.Sprintf("subs-%d", GetRandomNumber()) +) + +type templateData struct { + TestNamespace string + SecretName string + Connection string + DeploymentName string + TriggerAuthName string + ScaledObjectName string + TopicName string + SubscriptionName string + ServiceBusNamespace string +} + +const ( + secretTemplate = ` +apiVersion: v1 +kind: Secret +metadata: + name: {{.SecretName}} + namespace: {{.TestNamespace}} +type: Opaque +data: + connection: {{.Connection}} +` + + deploymentTemplate = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{.DeploymentName}} + namespace: {{.TestNamespace}} +spec: + replicas: 0 + selector: + matchLabels: + app: {{.DeploymentName}} + template: + metadata: + labels: + app: {{.DeploymentName}} + spec: + containers: + - name: nginx + image: nginx:1.16.1 +` + + triggerAuthTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: TriggerAuthentication +metadata: + name: {{.TriggerAuthName}} + namespace: {{.TestNamespace}} +spec: + podIdentity: + provider: azure-workload +` + + scaledObjectTemplate = ` +apiVersion: keda.sh/v1alpha1 +kind: ScaledObject +metadata: + name: {{.ScaledObjectName}} + namespace: {{.TestNamespace}} + labels: + deploymentName: {{.DeploymentName}} +spec: + scaleTargetRef: + name: {{.DeploymentName}} + pollingInterval: 5 + cooldownPeriod: 10 + minReplicaCount: 0 + maxReplicaCount: 1 + triggers: + - type: azure-servicebus + metadata: + topicName: {{.TopicName}} + subscriptionName: {{.SubscriptionName}} + namespace: {{.ServiceBusNamespace}} + activationMessageCount: "5" + authenticationRef: + name: {{.TriggerAuthName}} +` +) + +func TestScaler(t *testing.T) { + // setup + t.Log("--- setting up ---") + require.NotEmpty(t, connectionString, "TF_AZURE_SERVICE_BUS_CONNECTION_STRING env variable is required for service bus tests") + + client, adminClient, namespace := setupServiceBusTopicAndSubscription(t) + + kc := GetKubernetesClient(t) + data, templates := getTemplateData() + data.ServiceBusNamespace = namespace + + CreateKubernetesResources(t, kc, testNamespace, data, templates) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") + + // test scaling + testActivation(t, kc, client) + testScaleOut(t, kc, client) + testScaleIn(t, kc, adminClient) + + // cleanup + DeleteKubernetesResources(t, kc, testNamespace, data, templates) + cleanupServiceBusTopic(t, adminClient) +} + +func setupServiceBusTopicAndSubscription(t *testing.T) (*azservicebus.Client, *admin.Client, string) { + adminClient, err := admin.NewClientFromConnectionString(connectionString, nil) + assert.NoErrorf(t, err, "cannot connect to service bus namespace - %s", err) + + // Delete the topic if already exists + _, _ = adminClient.DeleteTopic(context.Background(), topicName, nil) + + _, err = adminClient.CreateTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot create the topic - %s", err) + _, err = adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil) + assert.NoErrorf(t, err, "cannot create the subscription - %s", err) + + namespace, err := adminClient.GetNamespaceProperties(context.Background(), nil) + assert.NoErrorf(t, err, "cannot get namespace info - %s", err) + + client, err := azservicebus.NewClientFromConnectionString(connectionString, nil) + assert.NoErrorf(t, err, "cannot connect to service bus namespace - %s", err) + + return client, adminClient, namespace.Name +} + +func getTemplateData() (templateData, []Template) { + base64ConnectionString := base64.StdEncoding.EncodeToString([]byte(connectionString)) + + return templateData{ + TestNamespace: testNamespace, + SecretName: secretName, + Connection: base64ConnectionString, + DeploymentName: deploymentName, + TriggerAuthName: triggerAuthName, + ScaledObjectName: scaledObjectName, + TopicName: topicName, + SubscriptionName: subscriptionName, + }, []Template{ + {Name: "secretTemplate", Config: secretTemplate}, + {Name: "deploymentTemplate", Config: deploymentTemplate}, + {Name: "triggerAuthTemplate", Config: triggerAuthTemplate}, + {Name: "scaledObjectTemplate", Config: scaledObjectTemplate}, + } +} + +func testActivation(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Client) { + t.Log("--- testing activation ---") + addMessages(t, client, 4) + + AssertReplicaCountNotChangeDuringTimePeriod(t, kc, deploymentName, testNamespace, 0, 60) +} + +func testScaleOut(t *testing.T, kc *kubernetes.Clientset, client *azservicebus.Client) { + t.Log("--- testing scale out ---") + addMessages(t, client, 10) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 1, 60, 1), + "replica count should be 1 after 1 minute") +} + +func testScaleIn(t *testing.T, kc *kubernetes.Clientset, adminClient *admin.Client) { + t.Log("--- testing scale in ---") + + _, err := adminClient.DeleteTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot delete the topic - %s", err) + _, err = adminClient.CreateTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot create the topic - %s", err) + _, err = adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil) + assert.NoErrorf(t, err, "cannot create the subscription - %s", err) + + assert.True(t, WaitForDeploymentReplicaReadyCount(t, kc, deploymentName, testNamespace, 0, 60, 1), + "replica count should be 0 after 1 minute") +} + +func addMessages(t *testing.T, client *azservicebus.Client, count int) { + sender, err := client.NewSender(topicName, nil) + assert.NoErrorf(t, err, "cannot create the sender - %s", err) + for i := 0; i < count; i++ { + msg := fmt.Sprintf("Message - %d", i) + _ = sender.SendMessage(context.Background(), &azservicebus.Message{ + Body: []byte(msg), + }, nil) + } +} + +func cleanupServiceBusTopic(t *testing.T, adminClient *admin.Client) { + t.Log("--- cleaning up ---") + _, err := adminClient.DeleteTopic(context.Background(), topicName, nil) + assert.NoErrorf(t, err, "cannot delete service bus topic - %s", err) +} diff --git a/tests/utils/cleanup_test.go b/tests/utils/cleanup_test.go index efb3d44e53e..219dc2159c9 100644 --- a/tests/utils/cleanup_test.go +++ b/tests/utils/cleanup_test.go @@ -20,6 +20,19 @@ func TestRemoveKEDA(t *testing.T) { t.Log("KEDA removed successfully using 'make undeploy' command") } +func TestRemoveAadPodIdentityComponents(t *testing.T) { + if AzureRunAadPodIdentityTests == "" || AzureRunAadPodIdentityTests == StringFalse { + t.Skip("skipping as aad pod identity tests are disabled") + } + + _, err := ExecuteCommand(fmt.Sprintf("helm uninstall aad-pod-identity --namespace %s", AzureAdPodIdentityNamespace)) + require.NoErrorf(t, err, "cannot uninstall aad pod identity webhook - %s", err) + + KubeClient = GetKubernetesClient(t) + + DeleteNamespace(t, KubeClient, AzureAdPodIdentityNamespace) +} + func TestRemoveWorkloadIdentityComponents(t *testing.T) { if AzureRunWorkloadIdentityTests == "" || AzureRunWorkloadIdentityTests == StringFalse { t.Skip("skipping as workload identity tests are disabled") diff --git a/tests/utils/setup_test.go b/tests/utils/setup_test.go index 6bcc90f7561..718c8ca26ec 100644 --- a/tests/utils/setup_test.go +++ b/tests/utils/setup_test.go @@ -201,3 +201,33 @@ func TestVerifyKEDA(t *testing.T) { require.True(t, success, "expected KEDA deployments to start 3 pods successfully") } + +func TestSetupAadPodIdentityComponents(t *testing.T) { + if AzureRunAadPodIdentityTests == "" || AzureRunAadPodIdentityTests == StringFalse { + t.Skip("skipping as aad pod identity tests are disabled") + } + + _, err := ExecuteCommand("helm version") + require.NoErrorf(t, err, "helm is not installed - %s", err) + + _, err = ExecuteCommand("helm repo add aad-pod-identity https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts") + require.NoErrorf(t, err, "cannot add pod identity helm repo - %s", err) + + _, err = ExecuteCommand("helm repo update aad-pod-identity") + require.NoErrorf(t, err, "cannot update aad pod identity helm repo - %s", err) + + KubeClient = GetKubernetesClient(t) + CreateNamespace(t, KubeClient, AzureAdPodIdentityNamespace) + + _, err = ExecuteCommand(fmt.Sprintf("helm upgrade --install "+ + "aad-pod-identity aad-pod-identity/aad-pod-identity "+ + "--namespace %s --wait "+ + "--set azureIdentities.keda.type=0 "+ + "--set azureIdentities.keda.namespace=keda "+ + "--set azureIdentities.keda.clientID=%s "+ + "--set azureIdentities.keda.resourceID=%s "+ + "--set azureIdentities.keda.binding.selector=keda "+ + "--set azureIdentities.keda.binding.name=keda", + AzureAdPodIdentityNamespace, AzureADMsiClientID, AzureADMsiID)) + require.NoErrorf(t, err, "cannot install aad pod identity webhook - %s", err) +} diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/CHANGELOG.md b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/CHANGELOG.md index 5877e476f6f..c1e0cc9a185 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/CHANGELOG.md +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/CHANGELOG.md @@ -1,5 +1,30 @@ # Release History +## 1.3.0-beta.2 (2023-01-10) + +### Features Added +* Added `OnBehalfOfCredential` to support the on-behalf-of flow + ([#16642](https://github.com/Azure/azure-sdk-for-go/issues/16642)) + +### Bugs Fixed +* `AzureCLICredential` reports token expiration in local time (should be UTC) + +### Other Changes +* `AzureCLICredential` imposes its default timeout only when the `Context` + passed to `GetToken()` has no deadline +* Added `NewCredentialUnavailableError()`. This function constructs an error indicating + a credential can't authenticate and an encompassing `ChainedTokenCredential` should + try its next credential, if any. + +## 1.3.0-beta.1 (2022-12-13) + +### Features Added +* `WorkloadIdentityCredential` and `DefaultAzureCredential` support + Workload Identity Federation on Kubernetes. `DefaultAzureCredential` + support requires environment variable configuration as set by the + Workload Identity webhook. + ([#15615](https://github.com/Azure/azure-sdk-for-go/issues/15615)) + ## 1.2.0 (2022-11-08) ### Other Changes diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/README.md b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/README.md index 2df42c813a5..da0baa9add3 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/README.md +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/README.md @@ -55,8 +55,9 @@ an Azure AD access token. See [Credential Types](#credential-types "Credential T ![DefaultAzureCredential authentication flow](img/mermaidjs/DefaultAzureCredentialAuthFlow.svg) 1. **Environment** - `DefaultAzureCredential` will read account information specified via [environment variables](#environment-variables) and use it to authenticate. -2. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it. -3. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity. +1. **Workload Identity** - If the app is deployed on Kubernetes with environment variables set by the workload identity webhook, `DefaultAzureCredential` will authenticate the configured identity. +1. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it. +1. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity. > Note: `DefaultAzureCredential` is intended to simplify getting started with the SDK by handling common scenarios with reasonable default behaviors. Developers who want more control or whose scenario isn't served by the default settings should use other credential types. @@ -128,12 +129,13 @@ client := armresources.NewResourceGroupsClient("subscription ID", chain, nil) |[ChainedTokenCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ChainedTokenCredential)|Define custom authentication flows, composing multiple credentials |[EnvironmentCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)|Authenticate a service principal or user configured by environment variables |[ManagedIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential)|Authenticate the managed identity of an Azure resource +|[WorkloadIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential)|Authenticate a workload identity on Kubernetes ### Authenticating Service Principals |Credential|Usage |-|- -|[ClientAssertionCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.2.0-beta.2#ClientAssertionCredential)|Authenticate a service principal with a signed client assertion +|[ClientAssertionCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientAssertionCredential)|Authenticate a service principal with a signed client assertion |[ClientCertificateCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientCertificateCredential)|Authenticate a service principal with a certificate |[ClientSecretCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientSecretCredential)|Authenticate a service principal with a secret diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azidentity.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azidentity.go index 60c3b9a1ec6..9d72c4f7043 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azidentity.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azidentity.go @@ -30,6 +30,7 @@ const ( azureClientCertificatePath = "AZURE_CLIENT_CERTIFICATE_PATH" azureClientID = "AZURE_CLIENT_ID" azureClientSecret = "AZURE_CLIENT_SECRET" + azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE" azurePassword = "AZURE_PASSWORD" azureRegionalAuthorityName = "AZURE_REGIONAL_AUTHORITY_NAME" azureTenantID = "AZURE_TENANT_ID" @@ -41,7 +42,7 @@ const ( tenantIDValidationErr = "invalid tenantID. You can locate your tenantID by following the instructions listed here: https://docs.microsoft.com/partner-center/find-ids-and-domain-names" ) -func getConfidentialClient(clientID, tenantID string, cred confidential.Credential, co *azcore.ClientOptions, additionalOpts ...confidential.Option) (confidential.Client, error) { +var getConfidentialClient = func(clientID, tenantID string, cred confidential.Credential, co *azcore.ClientOptions, additionalOpts ...confidential.Option) (confidentialClient, error) { if !validTenantID(tenantID) { return confidential.Client{}, errors.New(tenantIDValidationErr) } @@ -58,7 +59,7 @@ func getConfidentialClient(clientID, tenantID string, cred confidential.Credenti return confidential.New(clientID, cred, o...) } -func getPublicClient(clientID, tenantID string, co *azcore.ClientOptions) (public.Client, error) { +var getPublicClient = func(clientID, tenantID string, co *azcore.ClientOptions) (public.Client, error) { if !validTenantID(tenantID) { return public.Client{}, errors.New(tenantIDValidationErr) } @@ -153,6 +154,7 @@ type confidentialClient interface { AcquireTokenSilent(ctx context.Context, scopes []string, options ...confidential.AcquireTokenSilentOption) (confidential.AuthResult, error) AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...confidential.AcquireTokenByAuthCodeOption) (confidential.AuthResult, error) AcquireTokenByCredential(ctx context.Context, scopes []string) (confidential.AuthResult, error) + AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string) (confidential.AuthResult, error) } // enables fakes for test scenarios diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_cli_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_cli_credential.go index 68f46d51a1e..3b0083a9b1d 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_cli_credential.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/azure_cli_credential.go @@ -100,8 +100,12 @@ func defaultTokenProvider() func(ctx context.Context, resource string, tenantID return nil, fmt.Errorf(`%s: unexpected scope "%s". Only alphanumeric characters and ".", ";", "-", and "/" are allowed`, credNameAzureCLI, resource) } - ctx, cancel := context.WithTimeout(ctx, timeoutCLIRequest) - defer cancel() + // 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, timeoutCLIRequest) + defer cancel() + } commandLine := "az account get-access-token -o json --resource " + resource if tenantID != "" { @@ -158,32 +162,17 @@ func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, e return azcore.AccessToken{}, err } - tokenExpirationDate, err := parseExpirationDate(t.ExpiresOn) + // the Azure CLI's "expiresOn" is local time + exp, err := time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local) if err != nil { - return azcore.AccessToken{}, fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err) + return azcore.AccessToken{}, fmt.Errorf("Error parsing token expiration time %q: %v", t.ExpiresOn, err) } converted := azcore.AccessToken{ Token: t.AccessToken, - ExpiresOn: *tokenExpirationDate, + ExpiresOn: exp.UTC(), } return converted, nil } -// parseExpirationDate parses either a Azure CLI or CloudShell date into a time object -func parseExpirationDate(input string) (*time.Time, error) { - // CloudShell (and potentially the Azure CLI in future) - expirationDate, cloudShellErr := time.Parse(time.RFC3339, input) - if cloudShellErr != nil { - // Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone) - const cliFormat = "2006-01-02 15:04:05.999999" - expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local) - if cliErr != nil { - return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr) - } - return &expirationDate, nil - } - return &expirationDate, nil -} - var _ azcore.TokenCredential = (*AzureCLICredential)(nil) diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/chained_token_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/chained_token_credential.go index 86a89064569..dacac7a2508 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/chained_token_credential.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/chained_token_credential.go @@ -81,10 +81,13 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token } } - var err error - var errs []error - var token azcore.AccessToken - var successfulCredential azcore.TokenCredential + var ( + err error + errs []error + successfulCredential azcore.TokenCredential + token azcore.AccessToken + unavailableErr *credentialUnavailableError + ) for _, cred := range c.sources { token, err = cred.GetToken(ctx, opts) if err == nil { @@ -93,12 +96,14 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token break } errs = append(errs, err) - if _, ok := err.(*credentialUnavailableError); !ok { + // continue to the next source iff this one returned credentialUnavailableError + if !errors.As(err, &unavailableErr) { break } } if c.iterating { c.cond.L.Lock() + // this is nil when all credentials returned an error c.successfulCredential = successfulCredential c.iterating = false c.cond.L.Unlock() @@ -108,7 +113,7 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token if err != nil { // return credentialUnavailableError iff all sources did so; return AuthenticationFailedError otherwise msg := createChainedErrorMessage(errs) - if _, ok := err.(*credentialUnavailableError); ok { + if errors.As(err, &unavailableErr) { err = newCredentialUnavailableError(c.name, msg) } else { res := getResponseFromError(err) diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_assertion_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_assertion_credential.go index ffcf2094be2..ea60901928d 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_assertion_credential.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/client_assertion_credential.go @@ -18,13 +18,15 @@ import ( const credNameAssertion = "ClientAssertionCredential" // ClientAssertionCredential authenticates an application with assertions provided by a callback function. -// This credential is for advanced scenarios. ClientCertificateCredential has a more convenient API for +// This credential is for advanced scenarios. [ClientCertificateCredential] has a more convenient API for // the most common assertion scenario, authenticating a service principal with a certificate. See // [Azure AD documentation] for details of the assertion format. // // [Azure AD documentation]: https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format type ClientAssertionCredential struct { client confidentialClient + // name enables replacing "ClientAssertionCredential" with "WorkloadIdentityCredential" in log messages + name string } // ClientAssertionCredentialOptions contains optional parameters for ClientAssertionCredential. @@ -49,7 +51,7 @@ func NewClientAssertionCredential(tenantID, clientID string, getAssertion func(c if err != nil { return nil, err } - return &ClientAssertionCredential{client: c}, nil + return &ClientAssertionCredential{client: c, name: credNameAssertion}, nil } // GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients. @@ -59,15 +61,15 @@ func (c *ClientAssertionCredential) GetToken(ctx context.Context, opts policy.To } ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes) if err == nil { - logGetTokenSuccess(c, opts) + logGetTokenSuccessImpl(c.name, opts) return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err } ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes) if err != nil { - return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameAssertion, err) + return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(c.name, err) } - logGetTokenSuccess(c, opts) + logGetTokenSuccessImpl(c.name, opts) return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err } diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/default_azure_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/default_azure_credential.go index c2b801c4a6d..2ae4610c5e1 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/default_azure_credential.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/default_azure_credential.go @@ -30,11 +30,15 @@ type DefaultAzureCredentialOptions struct { // DefaultAzureCredential is a default credential chain for applications that will deploy to Azure. // It combines credentials suitable for deployment with credentials suitable for local development. -// It attempts to authenticate with each of these credential types, in the following order, stopping when one provides a token: +// It attempts to authenticate with each of these credential types, in the following order, stopping +// when one provides a token: // -// EnvironmentCredential -// ManagedIdentityCredential -// AzureCLICredential +// - [EnvironmentCredential] +// - [WorkloadIdentityCredential], if environment variable configuration is set by the Azure workload +// identity webhook. Use [WorkloadIdentityCredential] directly when not using the webhook or needing +// more control over its configuration. +// - [ManagedIdentityCredential] +// - [AzureCLICredential] // // Consult the documentation for these credential types for more information on how they authenticate. // Once a credential has successfully authenticated, DefaultAzureCredential will use that credential for @@ -60,9 +64,35 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default creds = append(creds, &defaultCredentialErrorReporter{credType: "EnvironmentCredential", err: err}) } + // workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID + haveWorkloadConfig := false + clientID, haveClientID := os.LookupEnv(azureClientID) + if haveClientID { + if file, ok := os.LookupEnv(azureFederatedTokenFile); ok { + if _, ok := os.LookupEnv(azureAuthorityHost); ok { + if tenantID, ok := os.LookupEnv(azureTenantID); ok { + haveWorkloadConfig = true + workloadCred, err := NewWorkloadIdentityCredential(tenantID, clientID, file, &WorkloadIdentityCredentialOptions{ + ClientOptions: options.ClientOptions}, + ) + if err == nil { + creds = append(creds, workloadCred) + } else { + errorMessages = append(errorMessages, credNameWorkloadIdentity+": "+err.Error()) + creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err}) + } + } + } + } + } + if !haveWorkloadConfig { + err := errors.New("missing environment variables for workload identity. Check webhook and pod configuration") + creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err}) + } + o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions} - if ID, ok := os.LookupEnv(azureClientID); ok { - o.ID = ClientID(ID) + if haveClientID { + o.ID = ClientID(clientID) } msiCred, err := NewManagedIdentityCredential(o) if err == nil { diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/errors.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/errors.go index 6695f1b70e4..2fba3698277 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/errors.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/errors.go @@ -101,24 +101,31 @@ func (*AuthenticationFailedError) NonRetriable() { var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil) -// credentialUnavailableError indicates a credential can't attempt -// authentication because it lacks required data or state. +// credentialUnavailableError indicates a credential can't attempt authentication because it lacks required +// data or state type credentialUnavailableError struct { - credType string - message string + message string } +// newCredentialUnavailableError is an internal helper that ensures consistent error message formatting func newCredentialUnavailableError(credType, message string) error { - return &credentialUnavailableError{credType: credType, message: message} + msg := fmt.Sprintf("%s: %s", credType, message) + return &credentialUnavailableError{msg} } -func (e *credentialUnavailableError) Error() string { - return e.credType + ": " + e.message +// NewCredentialUnavailableError constructs an error indicating a credential can't attempt authentication +// because it lacks required data or state. When [ChainedTokenCredential] receives this error it will try +// its next credential, if any. +func NewCredentialUnavailableError(message string) error { + return &credentialUnavailableError{message} } -// NonRetriable indicates that this error should not be retried. -func (e *credentialUnavailableError) NonRetriable() { - // marker method +// Error implements the error interface. Note that the message contents are not contractual and can change over time. +func (e *credentialUnavailableError) Error() string { + return e.message } +// NonRetriable is a marker method indicating this error should not be retried. It has no implementation. +func (e *credentialUnavailableError) NonRetriable() {} + var _ errorinfo.NonRetriable = (*credentialUnavailableError)(nil) diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/interactive_browser_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/interactive_browser_credential.go index 9032ae9886a..5c20e332e61 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/interactive_browser_credential.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/interactive_browser_credential.go @@ -27,8 +27,9 @@ type InteractiveBrowserCredentialOptions struct { // ClientID is the ID of the application users will authenticate to. // Defaults to the ID of an Azure development application. ClientID string - // RedirectURL will be supported in a future version but presently doesn't work: https://github.com/Azure/azure-sdk-for-go/issues/15632. - // Applications which have "http://localhost" registered as a redirect URL need not set this option. + // RedirectURL is the URL Azure Active Directory will redirect to with the access token. This is required + // only when setting ClientID, and must match a redirect URI in the application's registration. + // Applications which have registered "http://localhost" as a redirect URI need not set this option. RedirectURL string } diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/logging.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/logging.go index 569453e4622..fbb44e320e5 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/logging.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/logging.go @@ -21,10 +21,13 @@ import ( const EventAuthentication log.Event = "Authentication" func logGetTokenSuccess(cred azcore.TokenCredential, opts policy.TokenRequestOptions) { - if !log.Should(EventAuthentication) { - return + logGetTokenSuccessImpl(fmt.Sprintf("%T", cred), opts) +} + +func logGetTokenSuccessImpl(credName string, opts policy.TokenRequestOptions) { + if log.Should(EventAuthentication) { + scope := strings.Join(opts.Scopes, ", ") + msg := fmt.Sprintf(`%s.GetToken() acquired a token for scope "%s"\n`, credName, scope) + log.Write(EventAuthentication, msg) } - scope := strings.Join(opts.Scopes, ", ") - msg := fmt.Sprintf("%T.GetToken() acquired a token for scope %s\n", cred, scope) - log.Write(EventAuthentication, msg) } diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed_identity_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed_identity_credential.go index 18078171ee8..3a64628b63a 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed_identity_credential.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/managed_identity_credential.go @@ -85,9 +85,7 @@ func NewManagedIdentityCredential(options *ManagedIdentityCredentialOptions) (*M return nil, err } cred := confidential.NewCredFromTokenProvider(mic.provideToken) - if err != nil { - return nil, err - } + // It's okay to give MSAL an invalid client ID because MSAL will use it only as part of a cache key. // ManagedIdentityClient handles all the details of authentication and won't receive this value from MSAL. clientID := "SYSTEM-ASSIGNED-MANAGED-IDENTITY" diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/on_behalf_of_credential.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/on_behalf_of_credential.go new file mode 100644 index 00000000000..c42269d0698 --- /dev/null +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/on_behalf_of_credential.go @@ -0,0 +1,88 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azidentity + +import ( + "context" + "crypto" + "crypto/x509" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential" +) + +const credNameOBO = "OnBehalfOfCredential" + +// OnBehalfOfCredential authenticates a service principal via the on-behalf-of flow. This is typically used by +// middle-tier services that authorize requests to other services with a delegated user identity. Because this +// is not an interactive authentication flow, an application using it must have admin consent for any delegated +// permissions before requesting tokens for them. See [Azure Active Directory documentation] for more details. +// +// [Azure Active Directory documentation]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow +type OnBehalfOfCredential struct { + assertion string + client confidentialClient +} + +// OnBehalfOfCredentialOptions contains optional parameters for OnBehalfOfCredential +type OnBehalfOfCredentialOptions struct { + azcore.ClientOptions + + // SendCertificateChain applies only when the credential is configured to authenticate with a certificate. + // This setting controls whether the credential sends the public certificate chain in the x5c header of each + // token request's JWT. This is required for, and only used in, Subject Name/Issuer (SNI) authentication. + SendCertificateChain bool +} + +// NewOnBehalfOfCredentialFromCertificate constructs an OnBehalfOfCredential that authenticates with a certificate. +// See [ParseCertificates] for help loading a certificate. +func NewOnBehalfOfCredentialFromCertificate(tenantID, clientID, userAssertion string, certs []*x509.Certificate, key crypto.PrivateKey, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) { + cred, err := confidential.NewCredFromCertChain(certs, key) + if err != nil { + return nil, err + } + return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options) +} + +// NewOnBehalfOfCredentialFromSecret constructs an OnBehalfOfCredential that authenticates with a client secret. +func NewOnBehalfOfCredentialFromSecret(tenantID, clientID, userAssertion, clientSecret string, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) { + cred, err := confidential.NewCredFromSecret(clientSecret) + if err != nil { + return nil, err + } + return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options) +} + +func newOnBehalfOfCredential(tenantID, clientID, userAssertion string, cred confidential.Credential, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) { + if options == nil { + options = &OnBehalfOfCredentialOptions{} + } + opts := []confidential.Option{} + if options.SendCertificateChain { + opts = append(opts, confidential.WithX5C()) + } + c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, opts...) + if err != nil { + return nil, err + } + return &OnBehalfOfCredential{assertion: userAssertion, client: c}, nil +} + +// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients. +func (o *OnBehalfOfCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { + if len(opts.Scopes) == 0 { + return azcore.AccessToken{}, errors.New(credNameSecret + ": GetToken() requires at least one scope") + } + ar, err := o.client.AcquireTokenOnBehalfOf(ctx, o.assertion, opts.Scopes) + if err != nil { + return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameOBO, err) + } + logGetTokenSuccess(o, opts) + return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, nil +} diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/version.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/version.go index 9757589d166..a436709fe3c 100644 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/version.go +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/version.go @@ -11,5 +11,5 @@ const ( component = "azidentity" // Version is the semantic version (see http://semver.org) of this module. - version = "v1.2.0" + version = "v1.3.0-beta.2" ) diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/workload_identity.go b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/workload_identity.go new file mode 100644 index 00000000000..e6af670f78f --- /dev/null +++ b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/workload_identity.go @@ -0,0 +1,83 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azidentity + +import ( + "context" + "os" + "sync" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" +) + +const credNameWorkloadIdentity = "WorkloadIdentityCredential" + +// WorkloadIdentityCredential supports Azure workload identity on Kubernetes. +// See [AKS documentation] for more information. +// +// [AKS documentation]: https://learn.microsoft.com/azure/aks/workload-identity-overview +type WorkloadIdentityCredential struct { + assertion, file string + cred *ClientAssertionCredential + expires time.Time + mtx *sync.RWMutex +} + +// WorkloadIdentityCredentialOptions contains optional parameters for WorkloadIdentityCredential. +type WorkloadIdentityCredentialOptions struct { + azcore.ClientOptions +} + +// NewWorkloadIdentityCredential constructs a WorkloadIdentityCredential. tenantID and clientID specify the identity the credential authenticates. +// file is a path to a file containing a Kubernetes service account token that authenticates the identity. +func NewWorkloadIdentityCredential(tenantID, clientID, file string, options *WorkloadIdentityCredentialOptions) (*WorkloadIdentityCredential, error) { + if options == nil { + options = &WorkloadIdentityCredentialOptions{} + } + w := WorkloadIdentityCredential{file: file, mtx: &sync.RWMutex{}} + cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &ClientAssertionCredentialOptions{ClientOptions: options.ClientOptions}) + if err != nil { + return nil, err + } + cred.name = credNameWorkloadIdentity + w.cred = cred + return &w, nil +} + +// GetToken requests an access token from Azure Active Directory. Azure SDK clients call this method automatically. +func (w *WorkloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { + return w.cred.GetToken(ctx, opts) +} + +// getAssertion returns the specified file's content, which is expected to be a Kubernetes service account token. +// Kubernetes is responsible for updating the file as service account tokens expire. +func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, error) { + w.mtx.RLock() + if w.expires.Before(time.Now()) { + // ensure only one goroutine at a time updates the assertion + w.mtx.RUnlock() + w.mtx.Lock() + defer w.mtx.Unlock() + // double check because another goroutine may have acquired the write lock first and done the update + if now := time.Now(); w.expires.Before(now) { + content, err := os.ReadFile(w.file) + if err != nil { + return "", err + } + w.assertion = string(content) + // Kubernetes rotates service account tokens when they reach 80% of their total TTL. The shortest TTL + // is 1 hour. That implies the token we just read is valid for at least 12 minutes (20% of 1 hour), + // but we add some margin for safety. + w.expires = now.Add(10 * time.Minute) + } + } else { + defer w.mtx.RUnlock() + } + return w.assertion, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1ea08a96500..66cf17ec545 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -87,7 +87,7 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming github.com/Azure/azure-sdk-for-go/sdk/azcore/to github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing -# github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.0 +# github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0-beta.2 ## explicit; go 1.18 github.com/Azure/azure-sdk-for-go/sdk/azidentity # github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1