Repository: Azure/azure-sdk-for-rust (rust)

Issue:
Remove TokenCredential::clear_cache()
Caching should be an implementation detail of each credential type. `clear_cache()` is misleading because some credentials don't cache anything, and clearing a cache doesn't guarantee the next `get_token` call will authenticate again or return a new token.

Pull request:
Remove TokenCredential::clear_cache
Closes #2381


Patch:
diff --git a/sdk/core/azure_core/CHANGELOG.md b/sdk/core/azure_core/CHANGELOG.md
index 33ba2df92..baee5d047 100644
--- a/sdk/core/azure_core/CHANGELOG.md
+++ b/sdk/core/azure_core/CHANGELOG.md
@@ -14,6 +14,7 @@
 
 ### Breaking Changes
 
+- Removed `azure_core::credentials::TokenCredential::clear_cache()`
 - Consolidated all the `tokio` features into a single feature named `tokio`. Traits remain separate but `tokio` support is enabled with a single feature.
 - Moved `AppendToUrlQuery` type under `http` module.
 - Moved `ByteStream` and `SeekableStream` types under `stream` module.
diff --git a/sdk/core/azure_core/src/credentials.rs b/sdk/core/azure_core/src/credentials.rs
index e66c81a34..e34ae3b4a 100644
--- a/sdk/core/azure_core/src/credentials.rs
+++ b/sdk/core/azure_core/src/credentials.rs
@@ -100,7 +100,4 @@ impl AccessToken {
 pub trait TokenCredential: Send + Sync + Debug {
     /// Gets a `AccessToken` for the specified resource
     async fn get_token(&self, scopes: &[&str]) -> crate::Result<AccessToken>;
-
-    /// Clear the credential's cache.
-    async fn clear_cache(&self) -> crate::Result<()>;
 }
diff --git a/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs b/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs
index d23fda607..24ee7bf8d 100644
--- a/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs
+++ b/sdk/cosmos/azure_data_cosmos/src/pipeline/authorization_policy.rs
@@ -175,10 +175,6 @@ mod tests {
                 OffsetDateTime::now_utc().saturating_add(time::Duration::minutes(5)),
             ))
         }
-
-        async fn clear_cache(&self) -> azure_core::Result<()> {
-            Ok(())
-        }
     }
 
     #[tokio::test]
diff --git a/sdk/identity/azure_identity/CHANGELOG.md b/sdk/identity/azure_identity/CHANGELOG.md
index cd1d79318..6d3802f0b 100644
--- a/sdk/identity/azure_identity/CHANGELOG.md
+++ b/sdk/identity/azure_identity/CHANGELOG.md
@@ -16,6 +16,7 @@
 - `ClientAssertionCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
 - Removed `get_subscription()` and `get_tenant()` from `AzureCliCredential`.
 - `WorkloadIdentityCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
+- Removed `clear_cache()` from all credential types
 
 ### Bugs Fixed
 
diff --git a/sdk/identity/azure_identity/examples/specific_credential.rs b/sdk/identity/azure_identity/examples/specific_credential.rs
index 54ae2e328..dfc8cc230 100644
--- a/sdk/identity/azure_identity/examples/specific_credential.rs
+++ b/sdk/identity/azure_identity/examples/specific_credential.rs
@@ -81,20 +81,6 @@ impl TokenCredential for SpecificAzureCredentialKind {
             }
         }
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        match self {
-            #[cfg(not(target_arch = "wasm32"))]
-            SpecificAzureCredentialKind::AzureCli(credential) => credential.clear_cache().await,
-            SpecificAzureCredentialKind::VirtualMachine(credential) => {
-                credential.clear_cache().await
-            }
-            SpecificAzureCredentialKind::AppService(credential) => credential.clear_cache().await,
-            SpecificAzureCredentialKind::WorkloadIdentity(credential) => {
-                credential.clear_cache().await
-            }
-        }
-    }
 }
 
 /// Define a credential that uses an environment variable named `AZURE_CREDENTIAL_KIND`
@@ -161,8 +147,4 @@ impl TokenCredential for SpecificAzureCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.source.get_token(scopes).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.source.clear_cache().await
-    }
 }
diff --git a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs
index f933033b8..add84581e 100644
--- a/sdk/identity/azure_identity/src/azure_pipelines_credential.rs
+++ b/sdk/identity/azure_identity/src/azure_pipelines_credential.rs
@@ -111,10 +111,6 @@ impl TokenCredential for AzurePipelinesCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.0.get_token(scopes).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.0.clear_cache().await
-    }
 }
 
 #[derive(Debug)]
diff --git a/sdk/identity/azure_identity/src/chained_token_credential.rs b/sdk/identity/azure_identity/src/chained_token_credential.rs
index ad6c38bcb..f961c4a1b 100644
--- a/sdk/identity/azure_identity/src/chained_token_credential.rs
+++ b/sdk/identity/azure_identity/src/chained_token_credential.rs
@@ -114,21 +114,6 @@ impl TokenCredential for ChainedTokenCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.cache.get_token(scopes, self.get_token(scopes)).await
     }
-
-    /// Clear the credential's cache.
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        // clear the internal cache as well as each of the underlying providers
-        self.cache.clear().await?;
-
-        for source in &self.sources {
-            source.clear_cache().await?;
-        }
-
-        // clear the successful credential if we are clearing the token cache
-        self.successful_credential.write().await.take();
-
-        Ok(())
-    }
 }
 
 fn format_aggregate_error(errors: &[Error]) -> String {
@@ -182,10 +167,6 @@ mod tests {
             *count += 1;
             Err(Error::message(ErrorKind::Credential, "failed to get token"))
         }
-
-        async fn clear_cache(&self) -> azure_core::Result<()> {
-            Ok(())
-        }
     }
 
     #[tokio::test]
diff --git a/sdk/identity/azure_identity/src/credentials/app_service_managed_identity_credential.rs b/sdk/identity/azure_identity/src/credentials/app_service_managed_identity_credential.rs
index c66001a70..301c512ad 100644
--- a/sdk/identity/azure_identity/src/credentials/app_service_managed_identity_credential.rs
+++ b/sdk/identity/azure_identity/src/credentials/app_service_managed_identity_credential.rs
@@ -55,8 +55,4 @@ impl TokenCredential for AppServiceManagedIdentityCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.credential.get_token(scopes).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.credential.clear_cache().await
-    }
 }
diff --git a/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs b/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs
index 9d48e7d14..c47f45497 100644
--- a/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/azure_cli_credentials.rs
@@ -293,11 +293,6 @@ impl TokenCredential for AzureCliCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.cache.get_token(scopes, self.get_token(scopes)).await
     }
-
-    /// Clear the credential's cache.
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.cache.clear().await
-    }
 }
 
 impl From<TokenCredentialOptions> for AzureCliCredentialOptions {
diff --git a/sdk/identity/azure_identity/src/credentials/cache.rs b/sdk/identity/azure_identity/src/credentials/cache.rs
index 161aca9d3..20fd9a1b2 100644
--- a/sdk/identity/azure_identity/src/credentials/cache.rs
+++ b/sdk/identity/azure_identity/src/credentials/cache.rs
@@ -15,6 +15,7 @@ impl TokenCache {
         Self(RwLock::new(HashMap::new()))
     }
 
+    #[allow(dead_code)]
     pub(crate) async fn clear(&self) -> azure_core::Result<()> {
         let mut token_cache = self.0.write().await;
         token_cache.clear();
diff --git a/sdk/identity/azure_identity/src/credentials/client_assertion_credentials.rs b/sdk/identity/azure_identity/src/credentials/client_assertion_credentials.rs
index 51941434b..63cec289a 100644
--- a/sdk/identity/azure_identity/src/credentials/client_assertion_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/client_assertion_credentials.rs
@@ -156,8 +156,4 @@ impl<C: ClientAssertion> TokenCredential for ClientAssertionCredential<C> {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.cache.get_token(scopes, self.get_token(scopes)).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.cache.clear().await
-    }
 }
diff --git a/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs b/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs
index 7a1dd0884..375e29140 100644
--- a/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/client_certificate_credentials.rs
@@ -357,8 +357,4 @@ impl TokenCredential for ClientCertificateCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.cache.get_token(scopes, self.get_token(scopes)).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.cache.clear().await
-    }
 }
diff --git a/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs b/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs
index 32ca3e2d8..5ac9047e2 100644
--- a/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/default_azure_credentials.rs
@@ -142,20 +142,6 @@ impl TokenCredential for DefaultAzureCredentialKind {
             }
         }
     }
-
-    /// Clear the credential's cache.
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        match self {
-            #[cfg(not(target_arch = "wasm32"))]
-            DefaultAzureCredentialKind::AzureCli(credential) => credential.clear_cache().await,
-            #[cfg(target_arch = "wasm32")]
-            _ => {
-                return Err(Error::with_message(ErrorKind::Credential, || {
-                    "No credential providers available"
-                }));
-            }
-        }
-    }
 }
 
 /// Provides a default `TokenCredential` authentication flow for applications that will be deployed to Azure.
@@ -226,18 +212,6 @@ impl TokenCredential for DefaultAzureCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.cache.get_token(scopes, self.get_token(scopes)).await
     }
-
-    /// Clear the credential's cache.
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        // clear the internal cache as well as each of the underlying providers
-        self.cache.clear().await?;
-
-        for source in &self.sources {
-            source.clear_cache().await?;
-        }
-
-        Ok(())
-    }
 }
 
 fn format_aggregate_error(errors: &[Error]) -> String {
diff --git a/sdk/identity/azure_identity/src/credentials/imds_managed_identity_credentials.rs b/sdk/identity/azure_identity/src/credentials/imds_managed_identity_credentials.rs
index 978dd9967..6f35c4898 100644
--- a/sdk/identity/azure_identity/src/credentials/imds_managed_identity_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/imds_managed_identity_credentials.rs
@@ -134,10 +134,6 @@ impl TokenCredential for ImdsManagedIdentityCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.cache.get_token(scopes, self.get_token(scopes)).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.cache.clear().await
-    }
 }
 
 fn expires_on_string<'de, D>(deserializer: D) -> std::result::Result<OffsetDateTime, D::Error>
diff --git a/sdk/identity/azure_identity/src/credentials/virtual_machine_managed_identity_credential.rs b/sdk/identity/azure_identity/src/credentials/virtual_machine_managed_identity_credential.rs
index ccd50e4a7..77b1954a7 100644
--- a/sdk/identity/azure_identity/src/credentials/virtual_machine_managed_identity_credential.rs
+++ b/sdk/identity/azure_identity/src/credentials/virtual_machine_managed_identity_credential.rs
@@ -43,8 +43,4 @@ impl TokenCredential for VirtualMachineManagedIdentityCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.credential.get_token(scopes).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.credential.clear_cache().await
-    }
 }
diff --git a/sdk/identity/azure_identity/src/credentials/workload_identity_credentials.rs b/sdk/identity/azure_identity/src/credentials/workload_identity_credentials.rs
index f69375d28..b0c74d8a3 100644
--- a/sdk/identity/azure_identity/src/credentials/workload_identity_credentials.rs
+++ b/sdk/identity/azure_identity/src/credentials/workload_identity_credentials.rs
@@ -107,10 +107,6 @@ impl TokenCredential for WorkloadIdentityCredential {
     async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
         self.0.get_token(scopes).await
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        self.0.clear_cache().await
-    }
 }
 
 // TODO: Should probably remove this once we consolidate and unify credentials.


Test patch:
diff --git a/eng/common/TestResources/New-TestResources.ps1 b/eng/common/TestResources/New-TestResources.ps1
index fce3bd020..ca2d33fe9 100755
--- a/eng/common/TestResources/New-TestResources.ps1
+++ b/eng/common/TestResources/New-TestResources.ps1
@@ -93,6 +93,11 @@ param (
     })]
     [array] $AllowIpRanges = @(),
 
+    # Instead of running the post script, create a wrapped file to run it with parameters
+    # so that CI can run it in a subsequent step with a refreshed azure login
+    [Parameter()]
+    [string] $SelfContainedPostScript,
+
     [Parameter()]
     [switch] $CI = ($null -ne $env:SYSTEM_TEAMPROJECTID),
 
@@ -625,9 +630,41 @@ try {
         SetResourceNetworkAccessRules -ResourceGroupName $ResourceGroupName -AllowIpRanges $AllowIpRanges -CI:$CI
 
         $postDeploymentScript = $templateFile.originalFilePath | Split-Path | Join-Path -ChildPath "$ResourceType-resources-post.ps1"
+
+        if ($SelfContainedPostScript -and !(Test-Path $postDeploymentScript)) {
+            throw "-SelfContainedPostScript is not supported if there is no 'test-resources-post.ps1' script in the deployment template directory"
+        }
+
         if (Test-Path $postDeploymentScript) {
-            Log "Invoking post-deployment script '$postDeploymentScript'"
-            &$postDeploymentScript -ResourceGroupName $ResourceGroupName -DeploymentOutputs $deploymentOutputs @PSBoundParameters
+            if ($SelfContainedPostScript) {
+                Log "Creating invokable post-deployment script '$SelfContainedPostScript' from '$postDeploymentScript'"
+
+                $deserialized = @{}
+                foreach ($parameter in $PSBoundParameters.GetEnumerator()) {
+                    if ($parameter.Value -is [System.Management.Automation.SwitchParameter]) {
+                        $deserialized[$parameter.Key] = $parameter.Value.ToBool()
+                    } else {
+                        $deserialized[$parameter.Key] = $parameter.Value
+                    }
+                }
+                $deserialized['ResourceGroupName'] = $ResourceGroupName
+                $deserialized['DeploymentOutputs'] = $deploymentOutputs
+                $serialized = $deserialized | ConvertTo-Json
+
+                $outScript = @"
+`$parameters = `@'
+$serialized
+'`@ | ConvertFrom-Json -AsHashtable
+# Set global variables that aren't always passed as parameters
+`$ResourceGroupName = `$parameters.ResourceGroupName
+`$DeploymentOutputs = `$parameters.DeploymentOutputs
+$postDeploymentScript `@parameters
+"@
+                $outScript | Out-File $SelfContainedPostScript
+            } else {
+                Log "Invoking post-deployment script '$postDeploymentScript'"
+                &$postDeploymentScript -ResourceGroupName $ResourceGroupName -DeploymentOutputs $deploymentOutputs @PSBoundParameters
+            }
         }
 
         if ($templateFile.jsonFilePath.EndsWith('.compiled.json')) {
diff --git a/eng/common/TestResources/deploy-test-resources.yml b/eng/common/TestResources/deploy-test-resources.yml
index 22a232d43..7a4593af8 100644
--- a/eng/common/TestResources/deploy-test-resources.yml
+++ b/eng/common/TestResources/deploy-test-resources.yml
@@ -9,6 +9,7 @@ parameters:
   ResourceType: test
   UseFederatedAuth: true
   PersistOidcToken: false
+  SelfContainedPostScript: self-contained-test-resources-post.ps1
 
 # SubscriptionConfiguration will be splatted into the parameters of the test
 # resources script. It should be JSON in the form:
@@ -89,6 +90,8 @@ steps:
           # Write the new SubscriptionConfiguration to be used by the remove test resources
           Write-Host "##vso[task.setvariable variable=SubscriptionConfiguration;]$($subscriptionConfiguration | ConvertTo-Json -Compress)"
 
+          $postScriptPath = $${{ parameters.PersistOidcToken }} ? '$(Agent.TempDirectory)/${{ parameters.SelfContainedPostScript }}' : $null
+
           # The subscriptionConfiguration may have ArmTemplateParameters defined, so
           # pass those in via the ArmTemplateParameters flag, and handle any
           # additional parameters from the pipelines via AdditionalParameters
@@ -100,10 +103,31 @@ steps:
             @subscriptionConfiguration `
             -AdditionalParameters ${{ parameters.ArmTemplateParameters }} `
             -AllowIpRanges ('$(azsdk-corp-net-ip-ranges)' -split ',') `
+            -SelfContainedPostScript $postScriptPath `
             -CI `
             -Force `
             -Verbose | Out-Null
 
+    - ${{ if eq(parameters.PersistOidcToken, true) }}:
+      # ARM deployments that take longer than 10-15 minutes (e.g. HSM) can
+      # cause post scripts to fail with expired credentials.
+      # Add a new task with a refreshed token as a workaround to this issue.
+      - task: AzureCLI@2
+        displayName: Test Resources Post with refreshed login
+        env:
+          ${{ insert }}: ${{ parameters.EnvVars }}
+        inputs:
+          azureSubscription: ${{ parameters.ServiceConnection }}
+          addSpnToEnvironment: true
+          scriptLocation: inlineScript
+          scriptType: pscore
+          inlineScript: |
+            $env:ARM_OIDC_TOKEN = $env:idToken
+            $scriptPath = '$(Agent.TempDirectory)/${{ parameters.SelfContainedPostScript }}'
+            Write-Host "Executing self contained test resources post script '$scriptPath'"
+            & $scriptPath
+            Remove-Item $scriptPath  # avoid any possible complications when we run multiple deploy templates
+
   - ${{ else }}:
     - pwsh: |
         eng/common/scripts/Import-AzModules.ps1
diff --git a/sdk/core/azure_core_test/src/credentials.rs b/sdk/core/azure_core_test/src/credentials.rs
index 6337d3ecb..90ba64f81 100644
--- a/sdk/core/azure_core_test/src/credentials.rs
+++ b/sdk/core/azure_core_test/src/credentials.rs
@@ -26,10 +26,6 @@ impl TokenCredential for MockCredential {
         );
         Ok(AccessToken { token, expires_on })
     }
-
-    async fn clear_cache(&self) -> azure_core::Result<()> {
-        Ok(())
-    }
 }
 
 /// Gets a `TokenCredential` appropriate for the current environment.