Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SecretClient.GetSecret with RequestContext #33767

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1659,7 +1659,7 @@ internal virtual Response<CertificateOperationProperties> GetPendingCertificate(
{
case 200:
case 403:
return _pipeline.CreateResponse(response, new CertificateOperationProperties());
return KeyVaultPipeline.CreateResponse(response, new CertificateOperationProperties());

case 404:
return Response.FromValue<CertificateOperationProperties>(null, response);
Expand Down Expand Up @@ -1690,7 +1690,7 @@ internal virtual async Task<Response<CertificateOperationProperties>> GetPending
{
case 200:
case 403:
return _pipeline.CreateResponse(response, new CertificateOperationProperties());
return KeyVaultPipeline.CreateResponse(response, new CertificateOperationProperties());

case 404:
return Response.FromValue<CertificateOperationProperties>(null, response);
Expand Down
2 changes: 2 additions & 0 deletions sdk/keyvault/Azure.Security.KeyVault.Secrets/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Added overload for `SecretClient.GetSecret` and `GetSecretAsync` to pass `RequestContext` to avoid throwing or logging when a secret was not found. ([#25125](https://github.com/Azure/azure-sdk-for-net/issues/25125))

### Breaking Changes

### Bugs Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public SecretClient(System.Uri vaultUri, Azure.Core.TokenCredential credential,
public virtual Azure.AsyncPageable<Azure.Security.KeyVault.Secrets.SecretProperties> GetPropertiesOfSecretsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Pageable<Azure.Security.KeyVault.Secrets.SecretProperties> GetPropertiesOfSecretVersions(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.AsyncPageable<Azure.Security.KeyVault.Secrets.SecretProperties> GetPropertiesOfSecretVersionsAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.NullableResponse<Azure.Security.KeyVault.Secrets.KeyVaultSecret> GetSecret(string name, string version, Azure.RequestContext context) { throw null; }
public virtual Azure.Response<Azure.Security.KeyVault.Secrets.KeyVaultSecret> GetSecret(string name, string version = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.NullableResponse<Azure.Security.KeyVault.Secrets.KeyVaultSecret>> GetSecretAsync(string name, string version, Azure.RequestContext context) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response<Azure.Security.KeyVault.Secrets.KeyVaultSecret>> GetSecretAsync(string name, string version = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual Azure.Response PurgeDeletedSecret(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public virtual System.Threading.Tasks.Task<Azure.Response> PurgeDeletedSecretAsync(string name, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ description: Samples for the Azure.Security.KeyVault.Secrets client library.
- [Creating, getting, updating, and deleting secrets](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Secrets/samples/Sample1_HelloWorld.md)
- [Back up and restore a secret](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Secrets/samples/Sample2_BackupAndRestore.md)
- [Listing secrets, secret versions, and deleted secrets](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Secrets/samples/Sample3_GetSecrets.md)
- [Get a secret without throwing if it is not defined](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Secrets/samples/Sample4_GetSecretIfExists.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Get a secret without throwing if it is not defined
heaths marked this conversation as resolved.
Show resolved Hide resolved

This sample demonstrates how to get a secret without throwing an exception if it is not defined.
This may be useful in some application configuration managers that attempt to fetch key/value pairs from a collection of providers and when exceptions may break startup or are otherwise costly.
To better configure ASP.NET applications or other applications that use common [.NET Configuration], see our documentation on [Azure.Extensions.AspNetCore.Configuration.Secrets].
To get started, you'll need a URI to an Azure Key Vault. See the [README](https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/keyvault/Azure.Security.KeyVault.Secrets/README.md) for links and instructions.

## Creating a SecretClient

To create a new `SecretClient` to create get a secret, you need the endpoint to an Azure Key Vault and credentials.
You can use the [DefaultAzureCredential] to try a number of common authentication methods optimized for both running as a service and development.

In the sample below, you can set `keyVaultUrl` based on an environment variable, configuration setting, or any way that works for your application.

```C# Snippet:SecretsSample4SecretClient
var client = new SecretClient(new Uri(keyVaultUrl), new DefaultAzureCredential());
```

## Get a secret

To prevent throwing a `RequestFailedException` when a secret does not exist, or to alter other behaviors of a specific API call, you can create a `RequestContext` and pass that to the API call:
heaths marked this conversation as resolved.
Show resolved Hide resolved

```C# Snippet:SecretsSample4GetSecretIfExists
// Do not treat HTTP 404 responses as errors.
RequestContext context = new RequestContext();
context.AddClassifier(404, false);
heaths marked this conversation as resolved.
Show resolved Hide resolved

// Try getting the latest application connection string using the context above.
NullableResponse<KeyVaultSecret> response = client.GetSecret("appConnectionString", null, context);
if (response.HasValue)
{
KeyVaultSecret secret = response.Value;
Debug.WriteLine($"Secret is returned with name {secret.Name} and value {secret.Value}");
}
```

You can also do this asynchronously:

```C# Snippet:SecretsSample4GetSecretIfExistsAsync
// Do not treat HTTP 404 responses as errors.
RequestContext context = new RequestContext();
context.AddClassifier(404, false);

// Try getting the latest application connection string using the context above.
NullableResponse<KeyVaultSecret> response = await client.GetSecretAsync("appConnectionString", null, context);
if (response.HasValue)
{
KeyVaultSecret secret = response.Value;
Debug.WriteLine($"Secret is returned with name {secret.Name} and value {secret.Value}");
}
```

See samples for [RequestContext] for more ways you can customize specific API calls.

[.NET Configuration]: https://learn.microsoft.com/dotnet/core/extensions/configuration
[Azure.Extensions.AspNetCore.Configuration.Secrets]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/extensions/Azure.Extensions.AspNetCore.Configuration.Secrets/README.md
[DefaultAzureCredential]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/README.md
[RequestContext]: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Azure.Core/samples/RequestContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,79 @@ public virtual Response<KeyVaultSecret> GetSecret(string name, string version =
}

/// <summary>
/// Lists the properties of all enabled and disabled versions of the specified secret. You can use the returned <see cref="SecretProperties.Name"/> and <see cref="SecretProperties.Version"/> in subsequent calls to <see cref="GetSecretAsync"/>.
/// Get a specified secret from a given key vault, but does not throw an exception if the secret does not exist.
/// </summary>
/// <remarks>
/// The get operation is applicable to any secret stored in Azure Key Vault.
/// This operation requires the secrets/get permission.
/// </remarks>
/// <param name="name">The name of the secret.</param>
/// <param name="version">The version of the secret.</param>
/// <param name="context">A <see cref="RequestContext"/> controlling the request lifetime, error handling, and per-call pipeline policies.</param>
/// <returns>A response containing the <see cref="KeyVaultSecret"/> or <c>null</c> if not found.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
/// <exception cref="RequestFailedException">The server returned an error. See <see cref="Exception.Message"/> for details returned from the server.</exception>
#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken.
public virtual async Task<NullableResponse<KeyVaultSecret>> GetSecretAsync(string name, string version, RequestContext context)
#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken.
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
Argument.AssertNotNullOrEmpty(name, nameof(name));

using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(SecretClient)}.{nameof(GetSecret)}");
scope.AddAttribute("secret", name);
scope.AddAttribute("version", version);
scope.Start();

try
{
return await _pipeline.SendRequestAsync(RequestMethod.Get, context, () => new KeyVaultSecret(), SecretsPath, name, "/", version).ConfigureAwait(false);
heaths marked this conversation as resolved.
Show resolved Hide resolved
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}

/// <summary>
/// Get a specified secret from a given key vault, but does not throw an exception if the secret does not exist.
/// </summary>
/// <remarks>
/// The get operation is applicable to any secret stored in Azure Key Vault.
/// This operation requires the secrets/get permission.
/// </remarks>
/// <param name="name">The name of the secret.</param>
/// <param name="version">The version of the secret.</param>
/// <param name="context">A <see cref="RequestContext"/> controlling the request lifetime, error handling, and per-call pipeline policies.</param>
/// <returns>A response containing the <see cref="KeyVaultSecret"/> or <c>null</c> if not found.</returns>
/// <exception cref="ArgumentException"><paramref name="name"/> is an empty string.</exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/> is null.</exception>
/// <exception cref="RequestFailedException">The server returned an error. See <see cref="Exception.Message"/> for details returned from the server.</exception>
#pragma warning disable AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken.
public virtual NullableResponse<KeyVaultSecret> GetSecret(string name, string version, RequestContext context)
#pragma warning restore AZC0002 // DO ensure all service methods, both asynchronous and synchronous, take an optional CancellationToken parameter called cancellationToken.
{
Argument.AssertNotNullOrEmpty(name, nameof(name));

using DiagnosticScope scope = _pipeline.CreateScope($"{nameof(SecretClient)}.{nameof(GetSecret)}");
scope.AddAttribute("secret", name);
scope.AddAttribute("version", version);
scope.Start();

try
{
return _pipeline.SendRequest(RequestMethod.Get, context, () => new KeyVaultSecret(), SecretsPath, name, "/", version);
}
catch (Exception e)
{
scope.Failed(e);
throw;
}
}

/// <summary>
/// Lists the properties of all enabled and disabled versions of the specified secret. You can use the returned <see cref="SecretProperties.Name"/> and <see cref="SecretProperties.Version"/> in subsequent calls to <c>GetSecretAsync</c>.
/// </summary>
/// <remarks>
/// <para>
Expand All @@ -167,7 +239,7 @@ public virtual AsyncPageable<SecretProperties> GetPropertiesOfSecretVersionsAsyn
}

/// <summary>
/// Lists the properties of all enabled and disabled versions of the specified secret. You can use the returned <see cref="SecretProperties.Name"/> and <see cref="SecretProperties.Version"/> in subsequent calls to <see cref="GetSecret"/>.
/// Lists the properties of all enabled and disabled versions of the specified secret. You can use the returned <see cref="SecretProperties.Name"/> and <see cref="SecretProperties.Version"/> in subsequent calls to <c>GetSecretc</c>.
/// </summary>
/// <remarks>
/// <para>
Expand All @@ -194,7 +266,7 @@ public virtual Pageable<SecretProperties> GetPropertiesOfSecretVersions(string n
}

/// <summary>
/// Lists the properties of all enabled and disabled secrets in the specified vault. You can use the returned <see cref="SecretProperties.Name"/> in subsequent calls to <see cref="GetSecretAsync"/>.
/// Lists the properties of all enabled and disabled secrets in the specified vault. You can use the returned <see cref="SecretProperties.Name"/> in subsequent calls to <c>GetSecretAsync</c>.
/// </summary>
/// <remarks>
/// The Get Secrets operation is applicable to the entire vault. However, only
Expand All @@ -212,7 +284,7 @@ public virtual AsyncPageable<SecretProperties> GetPropertiesOfSecretsAsync(Cance
}

/// <summary>
/// Lists the properties of all enabled and disabled secrets in the specified vault. You can use the returned <see cref="SecretProperties.Name"/> in subsequent calls to <see cref="GetSecret"/>.
/// Lists the properties of all enabled and disabled secrets in the specified vault. You can use the returned <see cref="SecretProperties.Name"/> in subsequent calls to <c>GetSecret</c>.
/// </summary>
/// <remarks>
/// The Get Secrets operation is applicable to the entire vault. However, only
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class SampleFixture: SamplesBase<KeyVaultTestEnvironment>
public partial class HelloWorld : SampleFixture { }
public partial class BackupAndRestore : SampleFixture { }
public partial class GetSecrets : SampleFixture { }
public partial class GetSecretIfExists : SampleFixture { }
public partial class Snippets : SampleFixture { }
#pragma warning restore SA1402 // File may only contain a single type
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Azure.Core;
using Azure.Core.TestFramework;
using NUnit.Framework.Constraints;
using Azure.Core.Tests;

namespace Azure.Security.KeyVault.Secrets.Tests
{
Expand Down Expand Up @@ -479,5 +480,30 @@ public async Task AuthenticateCrossTenant()

Assert.AreEqual(200, response.GetRawResponse().Status);
}

[RecordedTest]
public async Task GetNonExistentSecretNoThrow()
annelo-msft marked this conversation as resolved.
Show resolved Hide resolved
{
RequestContext context = new();
context.AddClassifier(404, false);

ClientDiagnosticListener trace = new ClientDiagnosticListener(name => name.StartsWith("Azure."), IsAsync);
try
{
NullableResponse<KeyVaultSecret> response = await Client.GetSecretAsync("ShouldNotExist", null, context);

Assert.AreEqual(404, response.GetRawResponse().Status);
Assert.IsFalse(response.HasValue);
Assert.Throws<InvalidOperationException>(() => { var _ = response.Value; });
}
finally
{
// Unregister listener before enumerating scopes.
trace.Dispose();
}

var scope = trace.AssertScope($"{nameof(SecretClient)}.{nameof(SecretClient.GetSecret)}");
Assert.IsFalse(scope.IsFailed);
}
}
}
Loading