diff --git a/AzureKeyVaultRecoverySamples.csproj b/AzureKeyVaultRecoverySamples.csproj index e6625b9..d313d6d 100644 --- a/AzureKeyVaultRecoverySamples.csproj +++ b/AzureKeyVaultRecoverySamples.csproj @@ -1,4 +1,4 @@ - + Exe @@ -6,7 +6,8 @@ - + + diff --git a/ClientContext.cs b/ClientContext.cs index 4ef7566..a95fd2b 100644 --- a/ClientContext.cs +++ b/ClientContext.cs @@ -1,10 +1,8 @@ -using System; -using System.Configuration; -using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; -using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Rest; using Microsoft.Rest.Azure.Authentication; +using System; +using System.Threading.Tasks; namespace AzureKeyVaultRecoverySamples { @@ -60,7 +58,7 @@ public static ClientContext Build(string tenantId, string objectId, string appId /// /// /// - public static Task GetServiceCredentialsAsync( string tenantId, string applicationId, string appSecret ) + public static Task GetServiceCredentialsAsync(string tenantId, string applicationId, string appSecret) { if (_servicePrincipalCredential == null) { @@ -69,41 +67,11 @@ public static Task GetServiceCredentialsAsync( string return ApplicationTokenProvider.LoginSilentAsync( tenantId, - _servicePrincipalCredential, + _servicePrincipalCredential, ActiveDirectoryServiceSettings.Azure, TokenCache.DefaultShared); } - /// - /// Generic ADAL Authentication callback - /// - public static async Task AcquireTokenAsync(string authority, string resource, string scope) - { - if (_servicePrincipalCredential == null) - { - // read directly from config - var appId = ConfigurationManager.AppSettings[SampleConstants.ConfigKeys.ApplicationId]; - var spSecret = ConfigurationManager.AppSettings[SampleConstants.ConfigKeys.SPSecret]; - - _servicePrincipalCredential = new ClientCredential(appId, spSecret); - } - - AuthenticationContext ctx = new AuthenticationContext(authority, false, TokenCache.DefaultShared); - AuthenticationResult result = await ctx.AcquireTokenAsync(resource, _servicePrincipalCredential).ConfigureAwait(false); - - return result.AccessToken; - } - - /// - /// Generic authentication callback for a specific tenant - /// - /// Identifier of tenant where authentication takes place. - /// Authentication callback. - /// Consider moving this class out from Controllers.Core into a separate top-level lib. - public static Func> GetAuthenticationCallback(string authority, string resource, string scope) - { - return () => { return AcquireTokenAsync(authority, resource, scope); }; - } #endregion } } diff --git a/KeyVaultEntityRecoverySamples.cs b/KeyVaultEntityRecoverySamples.cs index e5a066a..c452709 100644 --- a/KeyVaultEntityRecoverySamples.cs +++ b/KeyVaultEntityRecoverySamples.cs @@ -1,12 +1,13 @@ -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Azure.KeyVault.Models; +using Azure; +using Azure.Security.KeyVault.Secrets; using Microsoft.Azure.Management.KeyVault.Fluent; using Microsoft.Azure.Management.KeyVault.Fluent.Models; using Microsoft.Azure.Management.ResourceManager.Fluent; using Microsoft.Rest.Azure; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; namespace AzureKeyVaultRecoverySamples { @@ -54,23 +55,26 @@ public static async Task DemonstrateRecoveryAndPurgeAsync() // retrieve the vault (or create, if it doesn't exist) var vault = await sample.CreateOrRetrieveVaultAsync(rgName, vaultName, enableSoftDelete: true, enablePurgeProtection: false); var vaultUri = vault.Properties.VaultUri; + + SecretClient secretClient = sample.GetDataClient(new Uri(vaultUri)); + Console.WriteLine("Operating with vault name '{0}' in resource group '{1}' and location '{2}'", vaultName, rgName, vault.Location); try { // set a secret Console.Write("Setting a new value for secret '{0}'...", secretName); - var secretResponse = await sample.DataClient.SetSecretWithHttpMessagesAsync(vaultUri, secretName, Guid.NewGuid().ToString()).ConfigureAwait(false); + await secretClient.SetSecretAsync(secretName, Guid.NewGuid().ToString()); Console.WriteLine("done."); // confirm existence Console.Write("Verifying secret creation..."); - var retrievedSecretResponse = await sample.DataClient.GetSecretWithHttpMessagesAsync(vaultUri, secretName, secretVersion: String.Empty).ConfigureAwait(false); + Response retrievedSecretResponse = await secretClient.GetSecretAsync(secretName); Console.WriteLine("done."); // confirm recovery is possible Console.Write("Verifying the secret deletion is recoverable..."); - var recoveryLevel = retrievedSecretResponse.Body.Attributes.RecoveryLevel; + var recoveryLevel = retrievedSecretResponse.Value.Properties.RecoveryLevel; if (!recoveryLevel.ToLowerInvariant().Contains("Recoverable".ToLowerInvariant())) { Console.WriteLine("failed; soft-delete is not enabled for this vault."); @@ -79,64 +83,41 @@ public static async Task DemonstrateRecoveryAndPurgeAsync() } Console.WriteLine("done."); + // delete secret Console.Write("Deleting secret..."); - await sample.DataClient.DeleteSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); - Console.WriteLine("done."); + DeleteSecretOperation deleteSecretOperation = await secretClient.StartDeleteSecretAsync(secretName); - // retrieve deleted secret; recoverable deletion is an asynchronous operation, during which the secret - // is not accessible, either as an active entity or a deleted one. Polling for up to 45s should be sufficient. - Console.Write("Retrieving the deleted secret..."); - AzureOperationResponse deletedSecretResponse = null; - await RetryHttpRequestAsync( - async () => { return deletedSecretResponse = await sample.DataClient.GetDeletedSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); }, - "get deleted secret", - SampleConstants.RetryPolicies.DefaultSoftDeleteRetryPolicy) - .ConfigureAwait(false); + // When deleting a secret asynchronously before you purge it, you can await the WaitForCompletionAsync method on the operation + await deleteSecretOperation.WaitForCompletionAsync(); Console.WriteLine("done."); // recover secret Console.Write("Recovering deleted secret..."); - var recoveredSecretResponse = await sample.DataClient.RecoverDeletedSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); + RecoverDeletedSecretOperation recoverDeletedSecretOperation = await secretClient.StartRecoverDeletedSecretAsync(secretName); + await recoverDeletedSecretOperation.WaitForCompletionAsync(); Console.WriteLine("done."); // confirm recovery Console.Write("Retrieving recovered secret..."); - await RetryHttpRequestAsync( - async () => { return retrievedSecretResponse = await sample.DataClient.GetSecretWithHttpMessagesAsync(vaultUri, secretName, secretVersion: String.Empty).ConfigureAwait(false); }, - "recover deleted secret", - SampleConstants.RetryPolicies.DefaultSoftDeleteRetryPolicy) - .ConfigureAwait(false); + await secretClient.GetSecretAsync(secretName); Console.WriteLine("done."); // delete secret - Console.Write("Deleting secret (pass #2)..."); - await sample.DataClient.DeleteSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); + Console.Write("Deleting recorvered secret..."); + DeleteSecretOperation deleteRecoveredSecretOperation = await secretClient.StartDeleteSecretAsync(secretName); + await deleteRecoveredSecretOperation.WaitForCompletionAsync(); Console.WriteLine("done."); // retrieve deleted secret - Console.Write("Retrieving the deleted secret (pass #2)..."); - await RetryHttpRequestAsync( - async () => { return deletedSecretResponse = await sample.DataClient.GetDeletedSecretWithHttpMessagesAsync(vaultUri, secretName); }, - "get deleted secret", - SampleConstants.RetryPolicies.DefaultSoftDeleteRetryPolicy) - .ConfigureAwait(false); - Console.WriteLine("done."); - - // purge secret - Console.Write("Purging deleted secret..."); - await sample.DataClient.PurgeDeletedSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); + Console.Write("Retrieving the deleted secret..."); + await secretClient.GetDeletedSecretAsync(secretName); Console.WriteLine("done."); } - catch (KeyVaultErrorException kvee) - { - Console.WriteLine("Unexpected KeyVault exception encountered: {0}", kvee.Message); + catch (RequestFailedException ex) - throw; - } - catch (CloudException ce) { - Console.WriteLine("Unexpected ARM exception encountered: {0}", ce.Message); + Console.WriteLine("Unexpected Key Vault exception encountered: {0}", ex.Message); throw; } @@ -166,50 +147,48 @@ public static async Task DemonstrateBackupAndRestoreAsync() // retrieve the vault (or create, if it doesn't exist) var vault = await sample.CreateOrRetrieveVaultAsync(rgName, vaultName, enableSoftDelete: false, enablePurgeProtection: false); var vaultUri = vault.Properties.VaultUri; + + SecretClient secretClient = sample.GetDataClient(new Uri(vaultUri)); + Console.WriteLine("Operating with vault name '{0}' in resource group '{1}' and location '{2}'", vaultName, rgName, vault.Location); try { // set a secret Console.Write("Setting a new value for secret '{0}'...", secretName); - var secretResponse = await sample.DataClient.SetSecretWithHttpMessagesAsync(vaultUri, secretName, Guid.NewGuid().ToString()).ConfigureAwait(false); + await secretClient.SetSecretAsync(secretName, Guid.NewGuid().ToString()); Console.WriteLine("done."); // confirm existence Console.Write("Verifying secret creation..."); - var retrievedSecretResponse = await sample.DataClient.GetSecretWithHttpMessagesAsync(vaultUri, secretName, secretVersion: String.Empty).ConfigureAwait(false); + await secretClient.GetSecretAsync(secretName); Console.WriteLine("done."); // backup secret Console.Write("Backing up secret..."); - var backupResponse = await sample.DataClient.BackupSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); + Response backupResponse = await secretClient.BackupSecretAsync(secretName); Console.WriteLine("done."); // delete secret Console.Write("Deleting secret..."); - await sample.DataClient.DeleteSecretWithHttpMessagesAsync(vaultUri, secretName).ConfigureAwait(false); + DeleteSecretOperation deleteSecretOperation = await secretClient.StartDeleteSecretAsync(secretName); + // When deleting a secret asynchronously before you purge it, you can await the WaitForCompletionAsync method on the operation + await deleteSecretOperation.WaitForCompletionAsync(); Console.WriteLine("done."); // restore secret Console.Write("Restoring secret from backup..."); - byte[] secretBackup = backupResponse.Body.Value; - var restoreResponse = await sample.DataClient.RestoreSecretWithHttpMessagesAsync(vaultUri, secretBackup).ConfigureAwait(false); + await secretClient.RestoreSecretBackupAsync(backupResponse.Value); Console.WriteLine("done."); // confirm existence Console.Write("Verifying secret restoration..."); - retrievedSecretResponse = await sample.DataClient.GetSecretWithHttpMessagesAsync(vaultUri, secretName, secretVersion: String.Empty).ConfigureAwait(false); + await secretClient.GetSecretAsync(secretName); Console.WriteLine("done."); } - catch (KeyVaultErrorException kvee) - { - Console.WriteLine("Unexpected KeyVault exception encountered: {0}", kvee.Message); - - throw; - } - catch (CloudException ce) + catch (RequestFailedException ex) { - Console.WriteLine("Unexpected ARM exception encountered: {0}", ce.Message); + Console.WriteLine("Unexpected Key Vault exception encountered: {0}", ex.Message); throw; } diff --git a/KeyVaultRecoverySamples.cs b/KeyVaultRecoverySamples.cs index 42bdb2d..8e03ea3 100644 --- a/KeyVaultRecoverySamples.cs +++ b/KeyVaultRecoverySamples.cs @@ -1,10 +1,10 @@ -using System; +using Microsoft.Azure.Management.KeyVault.Fluent; +using Microsoft.Azure.Management.KeyVault.Fluent.Models; +using Microsoft.Azure.Management.ResourceManager.Fluent; +using System; using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.Management.KeyVault.Fluent; -using Microsoft.Azure.Management.KeyVault.Fluent.Models; -using Microsoft.Azure.Management.ResourceManager.Fluent; namespace AzureKeyVaultRecoverySamples { @@ -25,7 +25,7 @@ public sealed class KeyVaultRecoverySamples : KeyVaultSampleBase /// Resource group name. /// Location of the vault. /// Vault name. - public KeyVaultRecoverySamples( string tenantId, string objectId, string appId, string appCredX5T, string subscriptionId, string resourceGroupName, string vaultLocation, string vaultName ) + public KeyVaultRecoverySamples(string tenantId, string objectId, string appId, string appCredX5T, string subscriptionId, string resourceGroupName, string vaultLocation, string vaultName) : base(tenantId, objectId, appId, appCredX5T, subscriptionId, resourceGroupName, vaultLocation, vaultName) { } @@ -79,7 +79,7 @@ public static async Task DemonstrateRecoveryAndPurgeForNewVaultAsync() Console.WriteLine("done."); // confirm the existence of the deleted vault - Console.Write("Retrieving deleted vault..."); + Console.Write("Retrieving deleted vault..."); deletedVault = await sample.ManagementClient.Vaults.GetDeletedAsync(vaultName, retrievedVault.Location).ConfigureAwait(false); Console.WriteLine("done; '{0}' deleted on: {1}, scheduled for purge on: {2}", deletedVault.Id, deletedVault.Properties.DeletionDate, deletedVault.Properties.ScheduledPurgeDate); diff --git a/KeyVaultSampleBase.cs b/KeyVaultSampleBase.cs index 26a1f76..d8454de 100644 --- a/KeyVaultSampleBase.cs +++ b/KeyVaultSampleBase.cs @@ -1,14 +1,15 @@ -using System; +using Azure; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using Microsoft.Azure.Management.KeyVault.Fluent; +using Microsoft.Azure.Management.KeyVault.Fluent.Models; +using Microsoft.Rest; +using System; using System.Collections.Generic; using System.Configuration; using System.Net; using System.Threading; using System.Threading.Tasks; -using Microsoft.Azure.KeyVault; -using Microsoft.Azure.KeyVault.Models; -using Microsoft.Azure.Management.KeyVault.Fluent; -using Microsoft.Azure.Management.KeyVault.Fluent.Models; -using Microsoft.Rest; namespace AzureKeyVaultRecoverySamples { @@ -30,7 +31,10 @@ public class KeyVaultSampleBase /// /// KeyVault data (Data Plane) client instance. /// - public KeyVaultClient DataClient { get; private set; } + public SecretClient GetDataClient(Uri vaultUri) + { + return new SecretClient(vaultUri, new DefaultAzureCredential()); + } /// /// Builds a sample object from the specified parameters. @@ -75,10 +79,7 @@ private void InstantiateSample(string tenantId, string objectId, string appId, s // instantiate the management client ManagementClient = new KeyVaultManagementClient(serviceCredentials); - ManagementClient.SubscriptionId = subscriptionId; - - // instantiate the data client - DataClient = new KeyVaultClient(ClientContext.AcquireTokenAsync); + ManagementClient.SubscriptionId = subscriptionId; } #region utilities @@ -139,23 +140,11 @@ public async Task EnableRecoveryOptionsOnExistingVaultAsync(string resourceGroup if (vault.Properties.EnableSoftDelete.HasValue && vault.Properties.EnableSoftDelete.Value) { - //if (!(vault.Properties.EnablePurgeProtection ^ enablePurgeProtection)) - //{ Console.WriteLine("The required recovery protection level is already enabled on vault {0}.", vaultName); - return; - //} - - // check if this is an attempt to lower the recovery level. - //if (vault.Properties.EnablePurgeProtection - // && !enablePurgeProtection) - //{ - // throw new InvalidOperationException("The recovery level on an existing vault cannot be lowered."); - //} } vault.Properties.EnableSoftDelete = true; - //vault.Properties.EnablePurgeProtection = enablePurgeProtection; // prepare the update operation on the vault var updateParameters = new VaultCreateOrUpdateParametersInner @@ -228,9 +217,9 @@ public async static Task RetryHttpRequestAsync( break; } - catch (KeyVaultErrorException kvee) + catch (RequestFailedException kvee) { - var statusCode = kvee.Response.StatusCode; + HttpStatusCode statusCode = (HttpStatusCode)kvee.Status; Console.Write("attempt #{0} to {1} returned: {2};", idx, functionName, statusCode); if (continueOn.Contains(statusCode)) diff --git a/README.md b/README.md index 7f5f247..7707259 100644 --- a/README.md +++ b/README.md @@ -4,68 +4,73 @@ languages: - csharp products: - azure +- azure-storage +- dotnet-core description: "This repo contains sample code demonstrating the backup/restore and recoverable deletion functionality of Azure Key Vault using the Azure .NET SDK." urlFragment: net-sdk-samples --- -# .NET SDK samples for recovering and restoring Azure Key Vault entities +# How to recover and restore Azure Key Vault entities with .NET -This repo contains sample code demonstrating the backup/restore and recoverable deletion functionality of Azure Key Vault using the [Azure .Net SDK](https://docs.microsoft.com/dotnet/api/overview/azure/key-vault?view=azure-dotnet). The scenarios covered by these samples include: +## Prerequisites -* Backing up and restoring Key Vault secrets and keys -* Enabling recoverable deletion on creating a new vault -* Enabling recoverable deletion on an existing vault -* Recovering or permanently deleting deleted vaults -* Recovering or permanently deleting Key Vault secrets, keys, and certificates +To complete this tutorial: -The recoverable deletion functionality is also referred to as 'soft delete'; consequently, a permanent, irrecoverable deletion is referred to as 'purge'. +* Install .NET Core 3.1 version for [Linux] or [Windows] -## Samples in this repo: +If you don't have an Azure subscription, create a [free account] before you begin. -* Back up and restore Key Vault entities -* Enable soft delete -* Delete, recover and purge a vault -* Delete, recover and purge vault entities +### Create an App registration using the Azure Portal -## Getting Started +1. Go to the [Azure Portal] and log in using your Azure account. +2. Search for and select **Azure Active Directory** > **Manage** > **App registrations**. +3. Select **New registration**. +4. Enter a name for your App registrations, then click **Register**. +5. Under **Overview** select **Application (client) ID**, **Directory (tenant) ID**, and **Object ID** copy to text editor for later use. +6. Under **Manage** > **Certificates & secrets** > **New client secret**, filter **Description** and click **Add**. +7. Copy preview created secret value to text editor for later use. -### Prerequisites +## Run the application +First, clone the repository on your machine: -- OS: Windows -- SDKs: - - Microsoft.Azure.Management.KeyVault.Fluent ver. 1.6.0+ - - KeyVault data SDK: Microsoft.Azure.KeyVault ver. 2.3.2+ -- Azure: - - a subscription, in which you have the KeyVaultContributor role - - an Azure Active Directory application, created in the tenant associated with the subscription, and with access to KeyVault; please see [Accessing Key Vault from a native application](https://blogs.technet.microsoft.com/kv/2016/09/17/accessing-key-vault-from-a-native-application) for details. - - the credentials of the AAD application, in the form of a client secret - +```bash +git clone https://github.com/Azure-Samples/key-vault-dotnet-recovery.git +``` -### Installation +Then, switch to the project folder to edit the app.config file, specifying the required parameters. +```bash +cd key-vault-dotnet-recovery +``` +Finally, run the application with the `dotnet run` command. -- open the solution in Visual Studio - NuGet should resolve the necessary packages +```console +dotnet run +``` +## This sample shows how to do following operations of Azure Key Vault +1. Back up and restore Key Vault entities. +2. Enable soft delete. +3. Delete, recover and purge a vault. +4. Delete, recover and purge vault entities. -### Quickstart -Follow these steps to get started with this sample: - -1. git clone https://github.com/Azure-Samples/key-vault-dotnet-recovery.git -2. cd key-vault-dotnet-recovery -4. edit the app.config file, specifying the tenant, subscription, AD app id, object id and client secret -5. dotnet run AzureKeyVaultRecoverySamples.csproj - - -## Demo - +The following samples are also related: -## Resources +- [Recovery scenario samples for Azure Key Vault using the Azure Python SDK] -Please see the following links for additional information: +## More information -- [Azure Key Vault soft-delete overview](https://docs.microsoft.com/azure/key-vault/key-vault-ovw-soft-delete) -- [How to use Key Vault soft-delete with PowerShell](https://docs.microsoft.com/azure/key-vault/key-vault-soft-delete-powershell) -- [How to use Key Vault soft-delete with CLI](https://docs.microsoft.com/azure/key-vault/key-vault-soft-delete-cli) +The [Azure Key Vault documentation] includes a rich set of tutorials and conceptual articles, which serve as a good complement to the samples. -The following samples are also related: +This project has adopted the [Microsoft Open Source Code of Conduct]. +For more information see the [Code of Conduct FAQ] or contact [opencode@microsoft.com] with any additional questions or comments. -- [Recovery scenario samples for Azure Key Vault using the Azure Python SDK](https://azure.microsoft.com/resources/samples/key-vault-recovery-python/) + +[Linux]: https://dotnet.microsoft.com/download +[Windows]: https://dotnet.microsoft.com/download +[free account]: https://azure.microsoft.com/free/?WT.mc_id=A261C142F +[Azure Portal]: https://portal.azure.com +[Recovery scenario samples for Azure Key Vault using the Azure Python SDK]: https://azure.microsoft.com/resources/samples/key-vault-recovery-python +[Azure Key Vault documentation]: https://docs.microsoft.com/azure/key-vault/general/basic-concepts +[Microsoft Open Source Code of Conduct]: https://opensource.microsoft.com/codeofconduct +[Code of Conduct FAQ]: https://opensource.microsoft.com/codeofconduct/faq +[opencode@microsoft.com]: mailto:opencode@microsoft.com \ No newline at end of file