Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 0 additions & 2 deletions .github/workflows/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
permissions:
id-token: write
contents: read
# issues: write
# pull-requests: write

jobs:
build-test-deploy:
Expand Down
2 changes: 2 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/AzureOpenAIProxy.ApiApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="$(AzureOpenAIVersion)" />
<PackageReference Include="Azure.Identity" Version="1.12.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.6.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="$(SwashbuckleVersion)" />
</ItemGroup>
Expand Down
7 changes: 6 additions & 1 deletion src/AzureOpenAIProxy.ApiApp/Configurations/AzureSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ public class AzureSettings
/// Gets or sets the <see cref="OpenAISettings"/> instance.
/// </summary>
public OpenAISettings OpenAI { get; set; } = new();
}

/// <summary>
/// Gets or sets the <see cref="KeyVaultSettings"/> instance.
/// </summary>
public KeyVaultSettings KeyVault { get; set; } = new();
}
22 changes: 22 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/Configurations/KeyVaultSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace AzureOpenAIProxy.ApiApp.Configurations;

/// <summary>
/// This represents the settings entity for Key Vault.
/// </summary>
public class KeyVaultSettings
{
/// <summary>
/// Gets the name of the configuration settings.
/// </summary>
public const string Name = "KeyVault";

/// <summary>
/// Gets or sets the Key Vault URI.
/// </summary>
public string? VaultUri { get; set; }

/// <summary>
/// Gets or sets the secret name.
/// </summary>
public string? SecretName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using AzureOpenAIProxy.ApiApp.Builders;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;

using AzureOpenAIProxy.ApiApp.Builders;
using AzureOpenAIProxy.ApiApp.Configurations;
using AzureOpenAIProxy.ApiApp.Filters;
using AzureOpenAIProxy.ApiApp.Services;
Expand All @@ -12,6 +15,29 @@ namespace AzureOpenAIProxy.ApiApp.Extensions;
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the KeyVault service to the service collection.
/// </summary>
/// <param name="services"><see cref="IServiceCollection"/> instance.</param>
/// <returns>Returns <see cref="IServiceCollection"/> instance.</returns>
public static IServiceCollection AddKeyVaultService(this IServiceCollection services)
{
services.AddScoped<SecretClient>(sp =>
{
var configuration = sp.GetService<IConfiguration>()
?? throw new InvalidOperationException($"{nameof(IConfiguration)} service is not registered.");

var settings = configuration.GetSection(AzureSettings.Name).GetSection(KeyVaultSettings.Name).Get<KeyVaultSettings>()
?? throw new InvalidOperationException($"{nameof(KeyVaultSettings)} could not be retrieved from the configuration.");

var client = new SecretClient(new Uri(settings.VaultUri!), new DefaultAzureCredential());

return client;
});

return services;
}

/// <summary>
/// Adds the OpenAI configuration settings to the service collection by reading appsettings.json.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

builder.AddServiceDefaults();

// Add KeyVault service
builder.Services.AddKeyVaultService();

// Add Azure OpenAI service.
builder.Services.AddOpenAIService();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
]
}
]
},
"KeyVault": {
"VaultUri": "https://{{key-vault-name}}.vault.azure.net/",
"SecretName": "azure-openai-instances"
}
}
}
4 changes: 4 additions & 0 deletions src/AzureOpenAIProxy.ApiApp/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
]
}
]
},
"KeyVault": {
"VaultUri": "https://{{key-vault-name}}.vault.azure.net/",
"SecretName": "azure-openai-instances"
}
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using Azure.Security.KeyVault.Secrets;

using AzureOpenAIProxy.ApiApp.Extensions;

using FluentAssertions;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace AzureOpenAIProxy.ApiApp.Tests.Extensions;

public class ServiceCollectionExtensionsTests
{
[Fact]
public void Given_ServiceCollection_When_Invoked_AddKeyVaultService_Then_It_Should_Contain_SecretClient()
{
// Arrange
var services = new ServiceCollection();

// Act
services.AddKeyVaultService();

// Assert
services.SingleOrDefault(p => p.ServiceType == typeof(SecretClient)).Should().NotBeNull();
}

[Fact]
public void Given_ServiceCollection_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception()
{
// Arrange
var services = new ServiceCollection();
services.AddKeyVaultService();

// Act
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();

// Assert
action.Should().Throw<InvalidOperationException>();
}

[Fact]
public void Given_Empty_AzureSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception()
{
// Arrange
var services = new ServiceCollection();
var dict = new Dictionary<string, string>()
{
{ "Azure", string.Empty },
};
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
services.AddSingleton<IConfiguration>(config);
services.AddKeyVaultService();

// Act
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();

// Assert
action.Should().Throw<InvalidOperationException>();
}

[Fact]
public void Given_Empty_KeyVaultSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Throw_Exception()
{
// Arrange
var services = new ServiceCollection();
var dict = new Dictionary<string, string>()
{
{ "Azure:KeyVault", string.Empty },
};
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
services.AddSingleton<IConfiguration>(config);
services.AddKeyVaultService();

// Act
Action action = () => services.BuildServiceProvider().GetService<SecretClient>();

// Assert
action.Should().Throw<InvalidOperationException>();
}

[Theory]
[InlineData("http://localhost")]
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_SecretClient(string vaultUri)
{
// Arrange
var services = new ServiceCollection();
var dict = new Dictionary<string, string>()
{
{ "Azure:KeyVault:VaultUri", vaultUri },
};
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
services.AddSingleton<IConfiguration>(config);
services.AddKeyVaultService();

// Act
var result = services.BuildServiceProvider().GetService<SecretClient>();

// Assert
result.Should().NotBeNull()
.And.BeOfType<SecretClient>();
}

[Theory]
[InlineData("http://localhost")]
public void Given_AppSettings_When_Invoked_AddKeyVaultService_Then_It_Should_Return_VaultUri(string vaultUri)
{
// Arrange
var services = new ServiceCollection();
var dict = new Dictionary<string, string>()
{
{ "Azure:KeyVault:VaultUri", vaultUri },
};
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
var config = new ConfigurationBuilder().AddInMemoryCollection(dict).Build();
#pragma warning restore CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
services.AddSingleton<IConfiguration>(config);
services.AddKeyVaultService();

var expected = new Uri(vaultUri);

// Act
var result = services.BuildServiceProvider().GetService<SecretClient>();

// Assert
result?.VaultUri.Should().BeEquivalentTo(expected);
}
}