diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs
index 659914a7ac..19598d16b4 100644
--- a/src/Config/ObjectModel/RuntimeConfig.cs
+++ b/src/Config/ObjectModel/RuntimeConfig.cs
@@ -353,10 +353,12 @@ public RuntimeConfig(
// This loader is not used as a part of hot reload and therefore does not need a handler.
FileSystemRuntimeConfigLoader loader = new(fileSystem, handler: null);
+ // Pass the parent's AKV options so @akv() references in child configs can
+ // be resolved using the parent's Key Vault configuration.
+ DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: this.AzureKeyVault, doReplaceEnvVar: true, doReplaceAkvVar: true);
+
foreach (string dataSourceFile in DataSourceFiles.SourceFiles)
{
- // Use default replacement settings for environment variable replacement
- DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true);
if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings))
{
diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
index d6f19ec65f..d7b67eb23d 100644
--- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
+++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
@@ -9,6 +9,7 @@
using System.Linq;
using System.Threading.Tasks;
using Azure.DataApiBuilder.Config;
+using Azure.DataApiBuilder.Config.Converters;
using Azure.DataApiBuilder.Config.ObjectModel;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
@@ -131,4 +132,108 @@ public async Task CanLoadValidMultiSourceConfigWithAutoentities(string configPat
Assert.IsTrue(runtimeConfig.SqlDataSourceUsed, "Should have Sql data source");
Assert.AreEqual(expectedEntities, runtimeConfig.Entities.Entities.Count, "Number of entities is not what is expected.");
}
+
+ ///
+ /// Validates that when a parent config has azure-key-vault options configured,
+ /// child configs can resolve @akv('...') references using the parent's AKV configuration.
+ /// Uses a local .akv file to simulate Azure Key Vault without requiring a real vault.
+ /// Regression test for https://github.com/Azure/data-api-builder/issues/3322
+ ///
+ [TestMethod]
+ public async Task ChildConfigResolvesAkvReferencesFromParentAkvOptions()
+ {
+ string akvFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".akv");
+ string childFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".json");
+
+ try
+ {
+ // Create a local .akv secrets file with test secrets.
+ await File.WriteAllTextAsync(akvFilePath, "my-connection-secret=Server=tcp:127.0.0.1,1433;Trusted_Connection=True;\n");
+
+ // Parent config with azure-key-vault pointing to the local .akv file.
+ string parentConfig = $@"{{
+ ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
+ ""data-source"": {{
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;""
+ }},
+ ""azure-key-vault"": {{
+ ""endpoint"": ""{akvFilePath.Replace("\\", "\\\\")}""
+ }},
+ ""data-source-files"": [""{childFilePath.Replace("\\", "\\\\")}""],
+ ""runtime"": {{
+ ""rest"": {{ ""enabled"": true }},
+ ""graphql"": {{ ""enabled"": true }},
+ ""host"": {{
+ ""cors"": {{ ""origins"": [] }},
+ ""authentication"": {{ ""provider"": ""StaticWebApps"" }}
+ }}
+ }},
+ ""entities"": {{}}
+ }}";
+
+ // Child config with @akv('...') reference in its connection string.
+ string childConfig = @"{
+ ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"",
+ ""data-source"": {
+ ""database-type"": ""mssql"",
+ ""connection-string"": ""@akv('my-connection-secret')""
+ },
+ ""runtime"": {
+ ""rest"": { ""enabled"": true },
+ ""graphql"": { ""enabled"": true },
+ ""host"": {
+ ""cors"": { ""origins"": [] },
+ ""authentication"": { ""provider"": ""StaticWebApps"" }
+ }
+ },
+ ""entities"": {
+ ""AkvChildEntity"": {
+ ""source"": ""dbo.AkvTable"",
+ ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""read""] }]
+ }
+ }
+ }";
+
+ await File.WriteAllTextAsync(childFilePath, childConfig);
+
+ MockFileSystem fs = new(new Dictionary()
+ {
+ { "dab-config.json", new MockFileData(parentConfig) }
+ });
+
+ FileSystemRuntimeConfigLoader loader = new(fs);
+
+ DeserializationVariableReplacementSettings replacementSettings = new(
+ azureKeyVaultOptions: new AzureKeyVaultOptions() { Endpoint = akvFilePath, UserProvidedEndpoint = true },
+ doReplaceEnvVar: true,
+ doReplaceAkvVar: true,
+ envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore);
+
+ Assert.IsTrue(
+ loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig, replacementSettings: replacementSettings),
+ "Config should load successfully when child config has @akv() references resolvable via parent AKV options.");
+
+ Assert.IsTrue(runtimeConfig.Entities.ContainsKey("AkvChildEntity"), "Child config entity should be merged into the parent config.");
+
+ // Verify the child's connection string was resolved from the .akv file.
+ string childDataSourceName = runtimeConfig.GetDataSourceNameFromEntityName("AkvChildEntity");
+ DataSource childDataSource = runtimeConfig.GetDataSourceFromDataSourceName(childDataSourceName);
+ Assert.IsTrue(
+ childDataSource.ConnectionString.Contains("127.0.0.1"),
+ "Child config connection string should have the AKV secret resolved.");
+ }
+ finally
+ {
+ if (File.Exists(akvFilePath))
+ {
+ File.Delete(akvFilePath);
+ }
+
+ if (File.Exists(childFilePath))
+ {
+ File.Delete(childFilePath);
+ }
+ }
+ }
}