Skip to content

Commit 9cdde7c

Browse files
authored
Add Key Vault client (aliencube#197)
1 parent 9c21cf6 commit 9cdde7c

File tree

9 files changed

+201
-4
lines changed

9 files changed

+201
-4
lines changed

.github/workflows/azure-dev.yml

-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ on:
1111
permissions:
1212
id-token: write
1313
contents: read
14-
# issues: write
15-
# pull-requests: write
1614

1715
jobs:
1816
build-test-deploy:

src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="Azure.AI.OpenAI" Version="$(AzureOpenAIVersion)" />
14+
<PackageReference Include="Azure.Identity" Version="1.12.0" />
15+
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
1416
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="$(AspNetCoreVersion)" />
1517
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(SwashbuckleVersion)" />
1618
</ItemGroup>

src/AzureOpenAIProxy.ApiApp/Configurations/AzureSettings.cs

+6-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ public class AzureSettings
1414
/// Gets or sets the <see cref="OpenAISettings"/> instance.
1515
/// </summary>
1616
public OpenAISettings OpenAI { get; set; } = new();
17-
}
17+
18+
/// <summary>
19+
/// Gets or sets the <see cref="KeyVaultSettings"/> instance.
20+
/// </summary>
21+
public KeyVaultSettings KeyVault { get; set; } = new();
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace AzureOpenAIProxy.ApiApp.Configurations;
2+
3+
/// <summary>
4+
/// This represents the settings entity for Key Vault.
5+
/// </summary>
6+
public class KeyVaultSettings
7+
{
8+
/// <summary>
9+
/// Gets the name of the configuration settings.
10+
/// </summary>
11+
public const string Name = "KeyVault";
12+
13+
/// <summary>
14+
/// Gets or sets the Key Vault URI.
15+
/// </summary>
16+
public string? VaultUri { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets the secret name.
20+
/// </summary>
21+
public string? SecretName { get; set; }
22+
}

src/AzureOpenAIProxy.ApiApp/Extensions/ServiceCollectionExtensions.cs

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using AzureOpenAIProxy.ApiApp.Builders;
1+
using Azure.Identity;
2+
using Azure.Security.KeyVault.Secrets;
3+
4+
using AzureOpenAIProxy.ApiApp.Builders;
25
using AzureOpenAIProxy.ApiApp.Configurations;
36
using AzureOpenAIProxy.ApiApp.Filters;
47
using AzureOpenAIProxy.ApiApp.Services;
@@ -12,6 +15,29 @@ namespace AzureOpenAIProxy.ApiApp.Extensions;
1215
/// </summary>
1316
public static class ServiceCollectionExtensions
1417
{
18+
/// <summary>
19+
/// Adds the KeyVault service to the service collection.
20+
/// </summary>
21+
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
22+
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
23+
public static IServiceCollection AddKeyVaultService(this IServiceCollection services)
24+
{
25+
services.AddScoped<SecretClient>(sp =>
26+
{
27+
var configuration = sp.GetService<IConfiguration>()
28+
?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered.");
29+
30+
var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get<KeyVaultSettings>()
31+
?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration.");
32+
33+
var client = new SecretClient(new Uri(settings.VaultUri!), new DefaultAzureCredential());
34+
35+
return client;
36+
});
37+
38+
return services;
39+
}
40+
1541
/// <summary>
1642
/// Adds the OpenAI configuration settings to the service collection by reading appsettings.json.
1743
/// </summary>

src/AzureOpenAIProxy.ApiApp/Program.cs

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
builder.AddServiceDefaults();
77

8+
// Add KeyVault service
9+
builder.Services.AddKeyVaultService();
10+
811
// Add Azure OpenAI service.
912
builder.Services.AddOpenAIService();
1013

src/AzureOpenAIProxy.ApiApp/appsettings.Development.sample.json

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
]
1919
}
2020
]
21+
},
22+
"KeyVault": {
23+
"VaultUri": "https://{{key-vault-name}}.vault.azure.net/",
24+
"SecretName": "azure-openai-instances"
2125
}
2226
}
2327
}

src/AzureOpenAIProxy.ApiApp/appsettings.json

+4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
]
1919
}
2020
]
21+
},
22+
"KeyVault": {
23+
"VaultUri": "https://{{key-vault-name}}.vault.azure.net/",
24+
"SecretName": "azure-openai-instances"
2125
}
2226
},
2327

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using Azure.Security.KeyVault.Secrets;
2+
3+
using AzureOpenAIProxy.ApiApp.Extensions;
4+
5+
using FluentAssertions;
6+
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.DependencyInjection;
9+
10+
namespace AzureOpenAIProxy.ApiApp.Tests.Extensions;
11+
12+
public class ServiceCollectionExtensionsTests
13+
{
14+
[Fact]
15+
public void Given_ServiceCollection_When_Invoked_AddKeyVaultService_Then_It_Should_Contain_SecretClient()
16+
{
17+
// Arrange
18+
var services = new ServiceCollection();
19+
20+
// Act
21+
services.AddKeyVaultService();
22+
23+
// Assert
24+
services.SingleOrDefault(p => p.ServiceType == typeof(SecretClient)).Should().NotBeNull();
25+
}
26+
27+
[Fact]
28+
public void Given_ServiceCollection_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception()
29+
{
30+
// Arrange
31+
var services = new ServiceCollection();
32+
services.AddKeyVaultService();
33+
34+
// Act
35+
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();
36+
37+
// Assert
38+
action.Should().Throw<InvalidOperationException>();
39+
}
40+
41+
[Fact]
42+
public void Given_Empty_AzureSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception()
43+
{
44+
// Arrange
45+
var services = new ServiceCollection();
46+
var dict = new Dictionary<string, string>()
47+
{
48+
{ "Azure", string.Empty },
49+
};
50+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
51+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
52+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
53+
services.AddSingleton<IConfiguration>(config);
54+
services.AddKeyVaultService();
55+
56+
// Act
57+
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();
58+
59+
// Assert
60+
action.Should().Throw<InvalidOperationException>();
61+
}
62+
63+
[Fact]
64+
public void Given_Empty_KeyVaultSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception()
65+
{
66+
// Arrange
67+
var services = new ServiceCollection();
68+
var dict = new Dictionary<string, string>()
69+
{
70+
{ "Azure:KeyVault", string.Empty },
71+
};
72+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
73+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
74+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
75+
services.AddSingleton<IConfiguration>(config);
76+
services.AddKeyVaultService();
77+
78+
// Act
79+
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();
80+
81+
// Assert
82+
action.Should().Throw<InvalidOperationException>();
83+
}
84+
85+
[Theory]
86+
[InlineData("http://localhost")]
87+
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_SecretClient(string vaultUri)
88+
{
89+
// Arrange
90+
var services = new ServiceCollection();
91+
var dict = new Dictionary<string, string>()
92+
{
93+
{ "Azure:KeyVault:VaultUri", vaultUri },
94+
};
95+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
96+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
97+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
98+
services.AddSingleton<IConfiguration>(config);
99+
services.AddKeyVaultService();
100+
101+
// Act
102+
var result = services.BuildServiceProvider().GetService<SecretClient>();
103+
104+
// Assert
105+
result.Should().NotBeNull()
106+
.And.BeOfType<SecretClient>();
107+
}
108+
109+
[Theory]
110+
[InlineData("http://localhost")]
111+
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_VaultUri(string vaultUri)
112+
{
113+
// Arrange
114+
var services = new ServiceCollection();
115+
var dict = new Dictionary<string, string>()
116+
{
117+
{ "Azure:KeyVault:VaultUri", vaultUri },
118+
};
119+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
120+
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
121+
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
122+
services.AddSingleton<IConfiguration>(config);
123+
services.AddKeyVaultService();
124+
125+
var expected = new Uri(vaultUri);
126+
127+
// Act
128+
var result = services.BuildServiceProvider().GetService<SecretClient>();
129+
130+
// Assert
131+
result?.VaultUri.Should().BeEquivalentTo(expected);
132+
}
133+
}

0 commit comments

Comments
 (0)