Repository: kserve/kserve

Issue:
Support more authentication approaches on Azure in Storage Initializer
/kind feature

**Describe the solution you'd like**
Currently only Azure Service Principal login is supported in Storage Initializer.
Customer is not able to use Managed Identity to download models from Azure Blobs.


**Anything else you would like to add:**
Azure Identity Python SDK supports variety of authentication approaches.
We may simply leverages the feature provided by the SDK.


Discussion:

Resolving patch:
diff --git a/python/kserve/kserve/api/creds_utils.py b/python/kserve/kserve/api/creds_utils.py
--- a/python/kserve/kserve/api/creds_utils.py
+++ b/python/kserve/kserve/api/creds_utils.py
@@ -132,10 +132,10 @@ def set_azure_credentials(namespace, credentials_file, service_account):
         azure_creds = json.load(azure_creds_file)
 
     data = {
-        'AZ_CLIENT_ID': azure_creds['clientId'],
-        'AZ_CLIENT_SECRET': azure_creds['clientSecret'],
-        'AZ_SUBSCRIPTION_ID': azure_creds['subscriptionId'],
-        'AZ_TENANT_ID': azure_creds['tenantId'],
+        'AZURE_CLIENT_ID': azure_creds['clientId'],
+        'AZURE_CLIENT_SECRET': azure_creds['clientSecret'],
+        'AZURE_SUBSCRIPTION_ID': azure_creds['subscriptionId'],
+        'AZURE_TENANT_ID': azure_creds['tenantId'],
         }
 
     secret_name = create_secret(
diff --git a/python/kserve/kserve/storage.py b/python/kserve/kserve/storage.py
--- a/python/kserve/kserve/storage.py
+++ b/python/kserve/kserve/storage.py
@@ -451,15 +451,23 @@ def _get_azure_storage_token():
         tenant_id = os.getenv("AZ_TENANT_ID", "")
         client_id = os.getenv("AZ_CLIENT_ID", "")
         client_secret = os.getenv("AZ_CLIENT_SECRET", "")
-        subscription_id = os.getenv("AZ_SUBSCRIPTION_ID", "")
 
-        if tenant_id == "" or client_id == "" or client_secret == "" or subscription_id == "":
+        # convert old environment variable to conform Azure defaults
+        # see azure/identity/_constants.py
+        if tenant_id:
+            os.environ["AZURE_TENANT_ID"] = tenant_id
+        if client_id:
+            os.environ["AZURE_CLIENT_ID"] = client_id
+        if client_secret:
+            os.environ["AZURE_CLIENT_SECRET"] = client_secret
+
+        client_id = os.getenv("AZURE_CLIENT_ID", "")
+        if not client_id:
             return None
 
         # note the SP must have "Storage Blob Data Owner" perms for this to work
-        from azure.identity import ClientSecretCredential
-        token_credential = ClientSecretCredential(tenant_id,
-                                                  client_id, client_secret)
+        from azure.identity import DefaultAzureCredential
+        token_credential = DefaultAzureCredential()
 
         logging.info("Retrieved SP token credential for client_id: %s",
                      client_id)


Test patch:
diff --git a/pkg/credentials/service_account_credentials_test.go b/pkg/credentials/service_account_credentials_test.go
--- a/pkg/credentials/service_account_credentials_test.go
+++ b/pkg/credentials/service_account_credentials_test.go
@@ -400,7 +400,7 @@ func TestGCSCredentialBuilder(t *testing.T) {
 	}
 }
 
-func TestAzureCredentialBuilder(t *testing.T) {
+func TestLegacyAzureCredentialBuilder(t *testing.T) {
 	g := gomega.NewGomegaWithT(t)
 	customOnlyServiceAccount := &v1.ServiceAccount{
 		ObjectMeta: metav1.ObjectMeta{
@@ -463,7 +463,7 @@ func TestAzureCredentialBuilder(t *testing.T) {
 														LocalObjectReference: v1.LocalObjectReference{
 															Name: "az-custom-secret",
 														},
-														Key: azure.AzureSubscriptionId,
+														Key: azure.LegacyAzureSubscriptionId,
 													},
 												},
 											},
@@ -474,7 +474,7 @@ func TestAzureCredentialBuilder(t *testing.T) {
 														LocalObjectReference: v1.LocalObjectReference{
 															Name: "az-custom-secret",
 														},
-														Key: azure.AzureTenantId,
+														Key: azure.LegacyAzureTenantId,
 													},
 												},
 											},
@@ -485,7 +485,7 @@ func TestAzureCredentialBuilder(t *testing.T) {
 														LocalObjectReference: v1.LocalObjectReference{
 															Name: "az-custom-secret",
 														},
-														Key: azure.AzureClientId,
+														Key: azure.LegacyAzureClientId,
 													},
 												},
 											},
@@ -496,7 +496,7 @@ func TestAzureCredentialBuilder(t *testing.T) {
 														LocalObjectReference: v1.LocalObjectReference{
 															Name: "az-custom-secret",
 														},
-														Key: azure.AzureClientSecret,
+														Key: azure.LegacyAzureClientSecret,
 													},
 												},
 											},
@@ -651,6 +651,146 @@ func TestHdfsCredentialBuilder(t *testing.T) {
 	g.Expect(c.Delete(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
 }
 
+func TestAzureCredentialBuilder(t *testing.T) {
+	g := gomega.NewGomegaWithT(t)
+	customOnlyServiceAccount := &v1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "custom-sa",
+			Namespace: "default",
+		},
+		Secrets: []v1.ObjectReference{
+			{
+				Name:      "az-custom-secret",
+				Namespace: "default",
+			},
+		},
+	}
+	customAzureSecret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "az-custom-secret",
+			Namespace: "default",
+		},
+		Data: map[string][]byte{
+			"AZURE_SUBSCRIPTION_ID": {},
+			"AZURE_TENANT_ID":       {},
+			"AZURE_CLIENT_ID":       {},
+			"AZURE_CLIENT_SECRET":   {},
+		},
+	}
+
+	scenarios := map[string]struct {
+		serviceAccount        *v1.ServiceAccount
+		inputConfiguration    *knservingv1.Configuration
+		expectedConfiguration *knservingv1.Configuration
+		shouldFail            bool
+	}{
+		"Custom Azure Secret": {
+			serviceAccount: customOnlyServiceAccount,
+			inputConfiguration: &knservingv1.Configuration{
+				Spec: knservingv1.ConfigurationSpec{
+					Template: knservingv1.RevisionTemplateSpec{
+						Spec: knservingv1.RevisionSpec{
+							PodSpec: v1.PodSpec{
+								Containers: []v1.Container{
+									{},
+								},
+							},
+						},
+					},
+				},
+			},
+			expectedConfiguration: &knservingv1.Configuration{
+				Spec: knservingv1.ConfigurationSpec{
+					Template: knservingv1.RevisionTemplateSpec{
+						Spec: knservingv1.RevisionSpec{
+							PodSpec: v1.PodSpec{
+								Containers: []v1.Container{
+									{
+										Env: []v1.EnvVar{
+											{
+												Name: azure.AzureSubscriptionId,
+												ValueFrom: &v1.EnvVarSource{
+													SecretKeyRef: &v1.SecretKeySelector{
+														LocalObjectReference: v1.LocalObjectReference{
+															Name: "az-custom-secret",
+														},
+														Key: azure.AzureSubscriptionId,
+													},
+												},
+											},
+											{
+												Name: azure.AzureTenantId,
+												ValueFrom: &v1.EnvVarSource{
+													SecretKeyRef: &v1.SecretKeySelector{
+														LocalObjectReference: v1.LocalObjectReference{
+															Name: "az-custom-secret",
+														},
+														Key: azure.AzureTenantId,
+													},
+												},
+											},
+											{
+												Name: azure.AzureClientId,
+												ValueFrom: &v1.EnvVarSource{
+													SecretKeyRef: &v1.SecretKeySelector{
+														LocalObjectReference: v1.LocalObjectReference{
+															Name: "az-custom-secret",
+														},
+														Key: azure.AzureClientId,
+													},
+												},
+											},
+											{
+												Name: azure.AzureClientSecret,
+												ValueFrom: &v1.EnvVarSource{
+													SecretKeyRef: &v1.SecretKeySelector{
+														LocalObjectReference: v1.LocalObjectReference{
+															Name: "az-custom-secret",
+														},
+														Key: azure.AzureClientSecret,
+													},
+												},
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			shouldFail: false,
+		},
+	}
+
+	g.Expect(c.Create(context.TODO(), customAzureSecret)).NotTo(gomega.HaveOccurred())
+	g.Expect(c.Create(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
+
+	builder := NewCredentialBulder(c, configMap)
+	for name, scenario := range scenarios {
+
+		err := builder.CreateSecretVolumeAndEnv(scenario.serviceAccount.Namespace, scenario.serviceAccount.Name,
+			&scenario.inputConfiguration.Spec.Template.Spec.Containers[0],
+			&scenario.inputConfiguration.Spec.Template.Spec.Volumes,
+		)
+		if scenario.shouldFail && err == nil {
+			t.Errorf("Test %q failed: returned success but expected error", name)
+		}
+		// Validate
+		if !scenario.shouldFail {
+			if err != nil {
+				t.Errorf("Test %q failed: returned error: %v", name, err)
+			}
+			if diff := cmp.Diff(scenario.expectedConfiguration, scenario.inputConfiguration); diff != "" {
+				t.Errorf("Test %q unexpected configuration spec (-want +got): %v", name, diff)
+			}
+		}
+	}
+
+	g.Expect(c.Delete(context.TODO(), customAzureSecret)).NotTo(gomega.HaveOccurred())
+	g.Expect(c.Delete(context.TODO(), customOnlyServiceAccount)).NotTo(gomega.HaveOccurred())
+}
+
 func TestAzureStorageAccessKeyCredentialBuilder(t *testing.T) {
 	g := gomega.NewGomegaWithT(t)
 	customOnlyServiceAccount := &v1.ServiceAccount{
diff --git a/test/e2e/credentials/test_set_creds.py b/test/e2e/credentials/test_set_creds.py
--- a/test/e2e/credentials/test_set_creds.py
+++ b/test/e2e/credentials/test_set_creds.py
@@ -123,7 +123,7 @@ def test_azure_credentials():
     created_sa = get_created_sa(sa_name)
     created_secret_name = created_sa.secrets[0].name
     created_secret = get_created_secret(created_secret_name)
-    assert created_secret.data['AZ_CLIENT_ID'] == 'dXNlcgo='
-    assert created_secret.data['AZ_CLIENT_SECRET'] == 'cGFzc3dvcmQ='
-    assert created_secret.data['AZ_SUBSCRIPTION_ID'] == 'MzMzMzMzMzMtMzMzMy0zMzMzLTMzMzMtMzMzMzMz'
-    assert created_secret.data['AZ_TENANT_ID'] == 'MTIzNAo='
+    assert created_secret.data['AZURE_CLIENT_ID'] == 'dXNlcgo='
+    assert created_secret.data['AZURE_CLIENT_SECRET'] == 'cGFzc3dvcmQ='
+    assert created_secret.data['AZURE_SUBSCRIPTION_ID'] == 'MzMzMzMzMzMtMzMzMy0zMzMzLTMzMzMtMzMzMzMz'
+    assert created_secret.data['AZURE_TENANT_ID'] == 'MTIzNAo='