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); + } + } + } }