diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs index 151d5babb2..64680b06d9 100644 --- a/src/Cli.Tests/EnvironmentTests.cs +++ b/src/Cli.Tests/EnvironmentTests.cs @@ -19,7 +19,13 @@ public class EnvironmentTests [TestInitialize] public void TestInitialize() { - StringJsonConverterFactory converterFactory = new(EnvironmentVariableReplacementFailureMode.Throw); + DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAKVVar: false, + envFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + + StringJsonConverterFactory converterFactory = new(replacementSettings); _options = new() { PropertyNameCaseInsensitive = true diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index a494bc38ae..6b5bdf0955 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs index 06d00b64d3..083d518ce3 100644 --- a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs +++ b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs @@ -12,9 +12,8 @@ namespace Azure.DataApiBuilder.Config.Converters; /// internal class AKVRetryPolicyOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +24,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AKVRetryPolicyOptionsConverter(_replaceEnvVar); + return new AKVRetryPolicyOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AKVRetryPolicyOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AKVRetryPolicyOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class AKVRetryPolicyOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AKVRetryPolicyOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AKVRetryPolicyOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -82,7 +80,7 @@ public AKVRetryPolicyOptionsConverter(bool replaceEnvVar) } else { - mode = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + mode = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); } break; diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs new file mode 100644 index 0000000000..fe1e762331 --- /dev/null +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// Converter factory for AzureKeyVaultOptions that can optionally perform variable replacement. +/// +internal class AzureKeyVaultOptionsConverterFactory : JsonConverterFactory +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + internal AzureKeyVaultOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) + { + _replacementSettings = replacementSettings; + } + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(AzureKeyVaultOptions)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new AzureKeyVaultOptionsConverter(_replacementSettings); + } + + private class AzureKeyVaultOptionsConverter : JsonConverter + { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) + { + _replacementSettings = replacementSettings; + } + + /// + /// Reads AzureKeyVaultOptions with optional variable replacement. + /// + public override AzureKeyVaultOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + string? endpoint = null; + AKVRetryPolicyOptions? retryPolicy = null; + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + return new AzureKeyVaultOptions + { + Endpoint = endpoint, + RetryPolicy = retryPolicy + }; + } + + string? property = reader.GetString(); + reader.Read(); + + switch (property) + { + case "endpoint": + if (reader.TokenType is JsonTokenType.String) + { + endpoint = reader.DeserializeString(_replacementSettings); + } + + break; + + case "retry-policy": + if (reader.TokenType is JsonTokenType.StartObject) + { + // Pass the replaceEnvVar setting to the retry policy converter + retryPolicy = JsonSerializer.Deserialize(ref reader, options); + } + + break; + + default: + reader.Skip(); + break; + } + } + } + else if (reader.TokenType is JsonTokenType.Null) + { + return null; + } + + throw new JsonException("Invalid AzureKeyVaultOptions format"); + } + + public override void Write(Utf8JsonWriter writer, AzureKeyVaultOptions value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, options); + } + } +} diff --git a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs index 1428c0d75f..d4b7623aa2 100644 --- a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs +++ b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs @@ -9,15 +9,14 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AzureLogAnalyticsAuthOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AzureLogAnalyticsAuthOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -48,7 +47,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "custom-table-name": if (reader.TokenType is not JsonTokenType.Null) { - customTableName = reader.DeserializeString(_replaceEnvVar); + customTableName = reader.DeserializeString(_replacementSettings); } break; @@ -56,7 +55,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "dcr-immutable-id": if (reader.TokenType is not JsonTokenType.Null) { - dcrImmutableId = reader.DeserializeString(_replaceEnvVar); + dcrImmutableId = reader.DeserializeString(_replacementSettings); } break; @@ -64,7 +63,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "dce-endpoint": if (reader.TokenType is not JsonTokenType.Null) { - dceEndpoint = reader.DeserializeString(_replaceEnvVar); + dceEndpoint = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs index 3fcbe8c7bd..fc7c72d655 100644 --- a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -12,9 +12,8 @@ namespace Azure.DataApiBuilder.Config.Converters; /// internal class AzureLogAnalyticsOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +24,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AzureLogAnalyticsOptionsConverter(_replaceEnvVar); + return new AzureLogAnalyticsOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AzureLogAnalyticsOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AzureLogAnalyticsOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class AzureLogAnalyticsOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AzureLogAnalyticsOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -57,7 +55,7 @@ internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) { if (reader.TokenType is JsonTokenType.StartObject) { - AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replaceEnvVar); + AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replacementSettings); bool? enabled = null; AzureLogAnalyticsAuthOptions? auth = null; @@ -91,7 +89,7 @@ internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) case "dab-identifier": if (reader.TokenType is not JsonTokenType.Null) { - logType = reader.DeserializeString(_replaceEnvVar); + logType = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/DataSourceConverterFactory.cs b/src/Config/Converters/DataSourceConverterFactory.cs index dabbee405e..1788ebf2b4 100644 --- a/src/Config/Converters/DataSourceConverterFactory.cs +++ b/src/Config/Converters/DataSourceConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class DataSourceConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new DataSourceConverter(_replaceEnvVar); + return new DataSourceConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal DataSourceConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal DataSourceConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class DataSourceConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public DataSourceConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public DataSourceConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override DataSource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -69,11 +67,11 @@ public DataSourceConverter(bool replaceEnvVar) switch (propertyName) { case "database-type": - databaseType = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + databaseType = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); break; case "connection-string": - connectionString = reader.DeserializeString(replaceEnvVar: _replaceEnvVar)!; + connectionString = reader.DeserializeString(_replacementSettings)!; break; case "health": @@ -106,7 +104,7 @@ public DataSourceConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { // Determine whether to resolve the environment variable or keep as-is. - string stringValue = reader.DeserializeString(replaceEnvVar: _replaceEnvVar)!; + string stringValue = reader.DeserializeString(_replacementSettings)!; if (bool.TryParse(stringValue, out bool boolValue)) { diff --git a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs index 52272c57a7..af1fd381aa 100644 --- a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class DataSourceHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +22,27 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new HealthCheckOptionsConverter(_replaceEnvVar); + return new HealthCheckOptionsConverter(_replacementSettings); } /// Whether to replace environment variable with its /// value or not while deserializing. - internal DataSourceHealthOptionsConvertorFactory(bool replaceEnvVar) + internal DataSourceHealthOptionsConvertorFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class HealthCheckOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - public HealthCheckOptionsConverter(bool replaceEnvVar) + public HealthCheckOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -85,7 +85,7 @@ public HealthCheckOptionsConverter(bool replaceEnvVar) case "name": if (reader.TokenType is not JsonTokenType.Null) { - name = reader.DeserializeString(_replaceEnvVar); + name = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs index 32a616ab81..7211780893 100644 --- a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs @@ -14,7 +14,7 @@ internal class EntityCacheOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +25,27 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityCacheOptionsConverter(_replaceEnvVar); + return new EntityCacheOptionsConverter(_replacementSettings); } /// Whether to replace environment variable with its /// value or not while deserializing. - internal EntityCacheOptionsConverterFactory(bool replaceEnvVar) + internal EntityCacheOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntityCacheOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - public EntityCacheOptionsConverter(bool replaceEnvVar) + public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -110,7 +110,7 @@ public EntityCacheOptionsConverter(bool replaceEnvVar) throw new JsonException("level property cannot be null."); } - level = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + level = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); break; } diff --git a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs index 576850b1cb..abe094e970 100644 --- a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntityGraphQLOptionsConverterFactory : JsonConverterFactory { - /// Determines whether to replace environment variable with its - /// value or not while deserializing. - private bool _replaceEnvVar; + /// Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityGraphQLOptionsConverter(_replaceEnvVar); + return new EntityGraphQLOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityGraphQLOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntityGraphQLOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntityGraphQLOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityGraphQLOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntityGraphQLOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -73,7 +71,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) case "type": if (reader.TokenType is JsonTokenType.String) { - singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + singular = reader.DeserializeString(_replacementSettings) ?? string.Empty; } else if (reader.TokenType is JsonTokenType.StartObject) { @@ -95,10 +93,10 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) switch (property2) { case "singular": - singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + singular = reader.DeserializeString(_replacementSettings) ?? string.Empty; break; case "plural": - plural = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + plural = reader.DeserializeString(_replacementSettings) ?? string.Empty; break; } } @@ -112,7 +110,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) break; case "operation": - string? op = reader.DeserializeString(_replaceEnvVar); + string? op = reader.DeserializeString(_replacementSettings); if (op is not null) { @@ -136,7 +134,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { - string? singular = reader.DeserializeString(_replaceEnvVar); + string? singular = reader.DeserializeString(_replacementSettings); return new EntityGraphQLOptions(singular ?? string.Empty, string.Empty); } diff --git a/src/Config/Converters/EntityRestOptionsConverterFactory.cs b/src/Config/Converters/EntityRestOptionsConverterFactory.cs index cc33943caa..f8c9096673 100644 --- a/src/Config/Converters/EntityRestOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityRestOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntityRestOptionsConverterFactory : JsonConverterFactory { - /// Determines whether to replace environment variable with its - /// value or not while deserializing. - private bool _replaceEnvVar; + /// Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityRestOptionsConverter(_replaceEnvVar); + return new EntityRestOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityRestOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntityRestOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } internal class EntityRestOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityRestOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntityRestOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -67,7 +65,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String || reader.TokenType is JsonTokenType.Null) { - restOptions = restOptions with { Path = reader.DeserializeString(_replaceEnvVar) }; + restOptions = restOptions with { Path = reader.DeserializeString(_replacementSettings) }; break; } @@ -87,7 +85,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) break; } - methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(replaceEnvVar: true)!)); + methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(new DeserializationVariableReplacementSettings())!)); } restOptions = restOptions with { Methods = methods.ToArray() }; @@ -107,7 +105,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { - return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replaceEnvVar), true); + return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replacementSettings), true); } if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) diff --git a/src/Config/Converters/EntitySourceConverterFactory.cs b/src/Config/Converters/EntitySourceConverterFactory.cs index 51af00717d..b8b9384130 100644 --- a/src/Config/Converters/EntitySourceConverterFactory.cs +++ b/src/Config/Converters/EntitySourceConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntitySourceConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,34 +21,33 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntitySourceConverter(_replaceEnvVar); + return new EntitySourceConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntitySourceConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntitySourceConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntitySourceConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntitySourceConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntitySourceConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override EntitySource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { - string? obj = reader.DeserializeString(_replaceEnvVar); + string? obj = reader.DeserializeString(_replacementSettings); return new EntitySource(obj ?? string.Empty, EntitySourceType.Table, new(), Array.Empty()); } diff --git a/src/Config/Converters/FileSinkConverter.cs b/src/Config/Converters/FileSinkConverter.cs index cc7d138a1b..4299fb913b 100644 --- a/src/Config/Converters/FileSinkConverter.cs +++ b/src/Config/Converters/FileSinkConverter.cs @@ -7,18 +7,17 @@ using Serilog; namespace Azure.DataApiBuilder.Config.Converters; + class FileSinkConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; - - /// - /// Whether to replace environment variable with its value or not while deserializing. - /// - public FileSinkConverter(bool replaceEnvVar) + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public FileSinkConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -59,7 +58,7 @@ public FileSinkConverter(bool replaceEnvVar) case "path": if (reader.TokenType is not JsonTokenType.Null) { - path = reader.DeserializeString(_replaceEnvVar); + path = reader.DeserializeString(_replacementSettings); } break; @@ -67,7 +66,7 @@ public FileSinkConverter(bool replaceEnvVar) case "rolling-interval": if (reader.TokenType is not JsonTokenType.Null) { - rollingInterval = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + rollingInterval = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); } break; diff --git a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs index 082c982e7e..109caef0d5 100644 --- a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class GraphQLRuntimeOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,25 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new GraphQLRuntimeOptionsConverter(_replaceEnvVar); + return new GraphQLRuntimeOptionsConverter(_replacementSettings); } - internal GraphQLRuntimeOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal GraphQLRuntimeOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class GraphQLRuntimeOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal GraphQLRuntimeOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override GraphQLRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -117,7 +117,7 @@ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) case "path": if (reader.TokenType is JsonTokenType.String) { - string? path = reader.DeserializeString(_replaceEnvVar); + string? path = reader.DeserializeString(_replacementSettings); if (path is null) { path = "/graphql"; diff --git a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs index d49cc264e7..2cfcd720e9 100644 --- a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class RuntimeHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,25 +22,25 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new HealthCheckOptionsConverter(_replaceEnvVar); + return new HealthCheckOptionsConverter(_replacementSettings); } - internal RuntimeHealthOptionsConvertorFactory(bool replaceEnvVar) + internal RuntimeHealthOptionsConvertorFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class HealthCheckOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - internal HealthCheckOptionsConverter(bool replaceEnvVar) + internal HealthCheckOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -102,7 +102,7 @@ internal HealthCheckOptionsConverter(bool replaceEnvVar) { if (reader.TokenType == JsonTokenType.String) { - string? currentRole = reader.DeserializeString(_replaceEnvVar); + string? currentRole = reader.DeserializeString(_replacementSettings); if (!string.IsNullOrEmpty(currentRole)) { stringList.Add(currentRole); diff --git a/src/Config/Converters/StringJsonConverterFactory.cs b/src/Config/Converters/StringJsonConverterFactory.cs index 078b611789..2e581ce55e 100644 --- a/src/Config/Converters/StringJsonConverterFactory.cs +++ b/src/Config/Converters/StringJsonConverterFactory.cs @@ -4,21 +4,20 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using Azure.DataApiBuilder.Service.Exceptions; namespace Azure.DataApiBuilder.Config.Converters; /// -/// Custom string json converter factory to replace environment variables of the pattern -/// @env('ENV_NAME') with their value during deserialization. +/// Custom string json converter factory to replace environment variables and other variable patterns +/// during deserialization using the DeserializationVariableReplacementSettings. /// public class StringJsonConverterFactory : JsonConverterFactory { - private EnvironmentVariableReplacementFailureMode _replacementFailureMode; + private DeserializationVariableReplacementSettings _replacementSettings; - public StringJsonConverterFactory(EnvironmentVariableReplacementFailureMode replacementFailureMode) + public StringJsonConverterFactory(DeserializationVariableReplacementSettings replacementSettings) { - _replacementFailureMode = replacementFailureMode; + _replacementSettings = replacementSettings; } public override bool CanConvert(Type typeToConvert) @@ -28,32 +27,16 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new StringJsonConverter(_replacementFailureMode); + return new StringJsonConverter(_replacementSettings); } class StringJsonConverter : JsonConverter { - // @env\(' : match @env(' - // .*? : lazy match any character except newline 0 or more times - // (?='\)) : look ahead for ') which will combine with our lazy match - // ie: in @env('hello')goodbye') we match @env('hello') - // '\) : consume the ') into the match (look ahead doesn't capture) - // This pattern lazy matches any string that starts with @env(' and ends with ') - // ie: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world') - // This matching pattern allows for the @env('') to be safely nested - // within strings that contain ') after our match. - // ie: if the environment variable "Baz" has the value of "Bar" - // fooBarBaz: "('foo@env('Baz')Baz')" would parse into - // fooBarBaz: "('fooBarBaz')" - // Note that there is no escape character currently for ') to exist - // within the name of the environment variable, but that ') is not - // a valid environment variable name in certain shells. - const string ENV_PATTERN = @"@env\('.*?(?='\))'\)"; - private EnvironmentVariableReplacementFailureMode _replacementFailureMode; + private DeserializationVariableReplacementSettings _replacementSettings; - public StringJsonConverter(EnvironmentVariableReplacementFailureMode replacementFailureMode) + public StringJsonConverter(DeserializationVariableReplacementSettings replacementSettings) { - _replacementFailureMode = replacementFailureMode; + _replacementSettings = replacementSettings; } public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -61,7 +44,18 @@ public StringJsonConverter(EnvironmentVariableReplacementFailureMode replacement if (reader.TokenType == JsonTokenType.String) { string? value = reader.GetString(); - return Regex.Replace(value!, ENV_PATTERN, new MatchEvaluator(ReplaceMatchWithEnvVariable)); + if (string.IsNullOrEmpty(value)) + { + return value; + } + + // Apply all replacement strategies configured in the settings + foreach (KeyValuePair> strategy in _replacementSettings.ReplacementStrategies) + { + value = strategy.Key.Replace(value, new MatchEvaluator(strategy.Value)); + } + + return value; } if (reader.TokenType == JsonTokenType.Null) @@ -76,30 +70,5 @@ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOp { writer.WriteStringValue(value); } - - private string ReplaceMatchWithEnvVariable(Match match) - { - // [^@env\(] : any substring that is not @env( - // .* : any char except newline any number of times - // (?=\)) : look ahead for end char of ) - // This pattern greedy matches all characters that are not a part of @env() - // ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world' - string innerPattern = @"[^@env\(].*(?=\))"; - - // strips first and last characters, ie: '''hello'' --> ''hello' - string envName = Regex.Match(match.Value, innerPattern).Value[1..^1]; - string? envValue = Environment.GetEnvironmentVariable(envName); - if (_replacementFailureMode == EnvironmentVariableReplacementFailureMode.Throw) - { - return envValue is not null ? envValue : - throw new DataApiBuilderException(message: $"Environmental Variable, {envName}, not found.", - statusCode: System.Net.HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - else - { - return envValue ?? match.Value; - } - } } } diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs index 20c6821d02..66c83de52d 100644 --- a/src/Config/Converters/Utf8JsonReaderExtensions.cs +++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs @@ -19,8 +19,7 @@ static internal class Utf8JsonReaderExtensions /// The result of deserialization. /// Thrown if the is not String. public static string? DeserializeString(this Utf8JsonReader reader, - bool replaceEnvVar, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings) { if (reader.TokenType is JsonTokenType.Null) { @@ -34,9 +33,9 @@ static internal class Utf8JsonReaderExtensions // Add the StringConverterFactory so that we can do environment variable substitution. JsonSerializerOptions options = new(); - if (replaceEnvVar) + if (replacementSettings is not null) { - options.Converters.Add(new StringJsonConverterFactory(replacementFailureMode)); + options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } return JsonSerializer.Deserialize(ref reader, options); diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs new file mode 100644 index 0000000000..8c548695a0 --- /dev/null +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.RegularExpressions; +using Azure.Core; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; +using Azure.DataApiBuilder.Config.Converters; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Service.Exceptions; + +namespace Azure.DataApiBuilder.Config +{ + public class DeserializationVariableReplacementSettings + { + public bool DoReplaceEnvVar { get; set; } = true; + public bool DoReplaceAKVVar { get; set; } = true; + public EnvironmentVariableReplacementFailureMode EnvFailureMode { get; set; } = EnvironmentVariableReplacementFailureMode.Throw; + + // @env\(' : match @env(' + // @AKV\(' : match @AKV(' + // .*? : lazy match any character except newline 0 or more times + // (?='\)) : look ahead for ')' which will combine with our lazy match + // ie: in @env('hello')goodbye') we match @env('hello') + // '\) : consume the ') into the match (look ahead doesn't capture) + // This pattern lazy matches any string that starts with @env(' and ends with ') + // ie: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world') + // This matching pattern allows for the @env('') to be safely nested + // within strings that contain ') after our match. + // ie: if the environment variable "Baz" has the value of "Bar" + // fooBarBaz: "('foo@env('Baz')Baz')" would parse into + // fooBarBaz: "('fooBarBaz')" + // Note that there is no escape character currently for ') to exist + // within the name of the environment variable, but that ') is not + // a valid environment variable name in certain shells. + public const string OUTER_ENV_PATTERN = @"@env\('.*?(?='\))'\)"; + public const string OUTER_AKV_PATTERN = @"@AKV\('.*?(?='\))'\)"; + + // [^@env\(] : any substring that is not @env( + // [^@AKV\(] : any substring that is not @AKV( + // .* : any char except newline any number of times + // (?=\)) : look ahead for end char of ) + // This pattern greedy matches all characters that are not a part of @env() + // ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world' + public const string INNER_ENV_PATTERN = @"[^@env\(].*(?=\))"; + public const string INNER_AKV_PATTERN = @"[^@AKV\(].*(?=\))"; + + private readonly AzureKeyVaultOptions? _azureKeyVaultOptions; + private readonly SecretClient? _akvClient; + + public Dictionary> ReplacementStrategies { get; private set; } = new(); + + public DeserializationVariableReplacementSettings( + AzureKeyVaultOptions? azureKeyVaultOptions = null, + bool doReplaceEnvVar = true, + bool doReplaceAKVVar = true, + EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + { + _azureKeyVaultOptions = azureKeyVaultOptions; + DoReplaceEnvVar = doReplaceEnvVar; + DoReplaceAKVVar = doReplaceAKVVar; + EnvFailureMode = envFailureMode; + + if (DoReplaceEnvVar) + { + ReplacementStrategies.Add( + new Regex(INNER_ENV_PATTERN, RegexOptions.Compiled), + ReplaceEnvVariable); + } + + if (DoReplaceAKVVar && _azureKeyVaultOptions is not null) + { + _akvClient = CreateSecretClient(_azureKeyVaultOptions); + ReplacementStrategies.Add( + new Regex(INNER_AKV_PATTERN, RegexOptions.Compiled), + ReplaceAKVVariable); + } + } + + private string ReplaceEnvVariable(Match match) + { + // strips first and last characters, ie: '''hello'' --> ''hello' + string name = Regex.Match(match.Value, INNER_ENV_PATTERN).Value[1..^1]; + string? value = Environment.GetEnvironmentVariable(name); + if (EnvFailureMode is EnvironmentVariableReplacementFailureMode.Throw) + { + return value is not null ? value : + throw new DataApiBuilderException( + message: $"Environmental Variable, {name}, not found.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + return value ?? match.Value; + } + } + + private string ReplaceAKVVariable(Match match) + { + // strips first and last characters, ie: '''hello'' --> ''hello' + string name = Regex.Match(match.Value, INNER_AKV_PATTERN).Value[1..^1]; + string? value = GetAKVVariable(name); + if (EnvFailureMode == EnvironmentVariableReplacementFailureMode.Throw) + { + return value is not null ? value : + throw new DataApiBuilderException(message: $"Azure Key Vault Variable, {name}, not found.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + return value ?? match.Value; + } + } + + private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) + { + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { + throw new DataApiBuilderException( + "Azure Key Vault endpoint must be specified.", + System.Net.HttpStatusCode.InternalServerError, + DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + SecretClientOptions clientOptions = new(); + + if (options.RetryPolicy is not null) + { + // Convert AKVRetryPolicyMode to RetryMode + RetryMode retryMode = options.RetryPolicy.Mode switch + { + AKVRetryPolicyMode.Fixed => RetryMode.Fixed, + AKVRetryPolicyMode.Exponential => RetryMode.Exponential, + null => RetryMode.Exponential, + _ => RetryMode.Exponential + }; + + clientOptions.Retry.Mode = retryMode; + clientOptions.Retry.MaxRetries = options.RetryPolicy.MaxCount ?? 3; + clientOptions.Retry.Delay = TimeSpan.FromSeconds(options.RetryPolicy.DelaySeconds ?? 1); + clientOptions.Retry.MaxDelay = TimeSpan.FromSeconds(options.RetryPolicy.MaxDelaySeconds ?? 16); + clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? 30); + } + + return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); + } + + private string? GetAKVVariable(string name) + { + if (_akvClient is null) + { + throw new InvalidOperationException("Azure Key Vault client is not initialized."); + } + + try + { + return _akvClient.GetSecret(name).Value.Value; + } + catch (Azure.RequestFailedException ex) when (ex.Status == 404) + { + return null; + } + } + } +} diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 9c2a8e50b5..c1f256c078 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -226,7 +226,8 @@ public bool TryLoadConfig( } } - if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar)) + if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, + new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true), logger: null, connectionString: _connectionString)) { if (TrySetupConfigFileWatcher()) { @@ -467,7 +468,7 @@ public override string GetPublishedDraftSchemaLink() string? schemaPath = _fileSystem.Path.Combine(assemblyDirectory, "dab.draft.schema.json"); string schemaFileContent = _fileSystem.File.ReadAllText(schemaPath); - Dictionary? jsonDictionary = JsonSerializer.Deserialize>(schemaFileContent, GetSerializationOptions()); + Dictionary? jsonDictionary = JsonSerializer.Deserialize>(schemaFileContent, GetSerializationOptions(replacementSettings: null)); if (jsonDictionary is null) { diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 7cf8159952..2bc35cd92a 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -420,7 +420,7 @@ public bool CheckDataSourceExists(string dataSourceName) public string ToJson(JsonSerializerOptions? jsonSerializerOptions = null) { // get default serializer options if none provided. - jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(); + jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(replacementSettings: null); return JsonSerializer.Serialize(this, jsonSerializerOptions); } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 4a220af0ea..1133cd2288 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -51,7 +51,7 @@ public RuntimeConfigLoader(HotReloadEventHandler? handler = /// DabChangeToken #pragma warning disable CA1024 // Use properties where appropriate public IChangeToken GetChangeToken() -#pragma warning restore CA1024 // Use properties where appropriate +#pragma warning restore CA1024 // Use properties where Appropriate { return _changeToken; } @@ -129,25 +129,91 @@ protected void SignalConfigChanged(string message = "") /// public abstract string GetPublishedDraftSchemaLink(); + /// + /// Extracts AzureKeyVaultOptions from JSON string with configurable variable replacement. + /// + /// JSON that represents the config file. + /// Whether to enable environment variable replacement during extraction. + /// Failure mode for environment variable replacement if enabled. + /// AzureKeyVaultOptions if present, null otherwise. + private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions(string json, + bool enableEnvReplacement, + Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode replacementFailureMode = Azure.DataApiBuilder.Config.Converters.EnvironmentVariableReplacementFailureMode.Throw) + { + JsonSerializerOptions options = new() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = new HyphenatedNamingPolicy(), + ReadCommentHandling = JsonCommentHandling.Skip + }; + DeserializationVariableReplacementSettings envOnlySettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: enableEnvReplacement, + doReplaceAKVVar: false, + envFailureMode: replacementFailureMode); + options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); + options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings: envOnlySettings)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings: envOnlySettings)); + + // Add environment variable replacement if enabled + if (enableEnvReplacement) + { + + } + + try + { + using JsonDocument doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("azure-key-vault", out JsonElement akvElement)) + { + return JsonSerializer.Deserialize(akvElement.GetRawText(), options); + } + } + catch + { + // If we can't extract AKV options, return null and proceed without AKV variable replacement + return null; + } + + return null; + } + /// /// Parses a JSON string into a RuntimeConfig object for single database scenario. /// /// JSON that represents the config file. /// The parsed config, or null if it parsed unsuccessfully. - /// True if the config was parsed, otherwise false. + /// Settings for variable replacement during deserialization. If null, no variable replacement will be performed. /// logger to log messages /// connectionString to add to config if specified - /// Whether to replace environment variable with its - /// value or not while deserializing. By default, no replacement happens. - /// Determines failure mode for env variable replacement. + /// True if the config was parsed, otherwise false. public static bool TryParseConfig(string json, [NotNullWhen(true)] out RuntimeConfig? config, + DeserializationVariableReplacementSettings? replacementSettings = null, ILogger? logger = null, - string? connectionString = null, - bool replaceEnvVar = false, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + string? connectionString = null) { - JsonSerializerOptions options = GetSerializationOptions(replaceEnvVar, replacementFailureMode); + // First pass: extract AzureKeyVault options if AKV replacement is requested + if (replacementSettings?.DoReplaceAKVVar == true) + { + AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( + json, + enableEnvReplacement: replacementSettings.DoReplaceEnvVar, + replacementFailureMode: replacementSettings.EnvFailureMode); + + // Update replacement settings with the extracted AKV options + if (azureKeyVaultOptions is not null) + { + replacementSettings = new DeserializationVariableReplacementSettings( + azureKeyVaultOptions: azureKeyVaultOptions, + doReplaceEnvVar: replacementSettings.DoReplaceEnvVar, + doReplaceAKVVar: replacementSettings.DoReplaceAKVVar, + envFailureMode: replacementSettings.EnvFailureMode); + } + } + + JsonSerializerOptions options = GetSerializationOptions(replacementSettings); try { @@ -180,11 +246,11 @@ public static bool TryParseConfig(string json, DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey); // Add Application Name for telemetry for MsSQL or PgSql - if (ds.DatabaseType is DatabaseType.MSSQL && replaceEnvVar) + if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true) { updatedConnection = GetConnectionStringWithApplicationName(connectionValue); } - else if (ds.DatabaseType is DatabaseType.PostgreSQL && replaceEnvVar) + else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true) { updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue); } @@ -225,11 +291,10 @@ ex is JsonException || /// /// Get Serializer options for the config file. /// - /// Whether to replace environment variable with value or not while deserializing. - /// By default, no replacement happens. + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. public static JsonSerializerOptions GetSerializationOptions( - bool replaceEnvVar = false, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings = null) { JsonSerializerOptions options = new() { @@ -241,31 +306,35 @@ public static JsonSerializerOptions GetSerializationOptions( Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replaceEnvVar)); - options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replaceEnvVar)); + options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replacementSettings)); + options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replacementSettings)); options.Converters.Add(new EntityHealthOptionsConvertorFactory()); options.Converters.Add(new RestRuntimeOptionsConverterFactory()); - options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntitySourceConverterFactory(replacementSettings)); + options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings)); options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); - options.Converters.Add(new EntityCacheOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); options.Converters.Add(new MultipleMutationOptionsConverter(options)); - options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar)); + options.Converters.Add(new DataSourceConverterFactory(replacementSettings)); options.Converters.Add(new HostOptionsConvertorFactory()); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replaceEnvVar)); - options.Converters.Add(new FileSinkConverter(replaceEnvVar)); - - if (replaceEnvVar) + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings)); + options.Converters.Add(new FileSinkConverter(replacementSettings)); + + // Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings)); + + // Only add the extensible string converter if we have replacement settings + if (replacementSettings is not null) { - options.Converters.Add(new StringJsonConverterFactory(replacementFailureMode)); + options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } return options; diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index faeb2b94d0..b46a716f48 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Net; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.NamingPolicies; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; @@ -189,8 +188,7 @@ public async Task Initialize( if (RuntimeConfigLoader.TryParseConfig( configuration, out RuntimeConfig? runtimeConfig, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Ignore)) + replacementSettings: null)) { _configLoader.RuntimeConfig = runtimeConfig; @@ -257,8 +255,7 @@ public async Task Initialize( string? graphQLSchema, string connectionString, string? accessToken, - bool replaceEnvVar = true, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings) { if (string.IsNullOrEmpty(connectionString)) { @@ -272,7 +269,7 @@ public async Task Initialize( IsLateConfigured = true; - if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replaceEnvVar: replaceEnvVar, replacementFailureMode: replacementFailureMode)) + if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replacementSettings)) { _configLoader.RuntimeConfig = runtimeConfig.DataSource.DatabaseType switch { diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index da600c9f63..e1cd799945 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,6 +5,7 @@ + @@ -78,4 +79,4 @@ - \ No newline at end of file + diff --git a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs index 2780af63c5..a6daebf3e4 100644 --- a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs +++ b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs @@ -5,7 +5,6 @@ using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -56,10 +55,7 @@ public void EntityCacheOptionsDeserialization_ValidJson( RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -103,10 +99,7 @@ public void EntityCacheOptionsDeserialization_InvalidValues(string entityCacheCo bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( json: fullConfig, out _, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsFalse(isParsingSuccessful, message: "Expected JSON parsing to fail."); @@ -141,10 +134,7 @@ public void GlobalCacheOptionsDeserialization_ValidValues( RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -187,10 +177,7 @@ public void GlobalCacheOptionsDeserialization_InvalidValues(string globalCacheCo bool parsingSuccessful = RuntimeConfigLoader.TryParseConfig( json: fullConfig, out _, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsFalse(parsingSuccessful, message: "Expected JSON parsing to fail."); @@ -216,10 +203,7 @@ public void GlobalCacheOptionsOverridesEntityCacheOptions(string globalCacheConf RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -252,10 +236,7 @@ public void UserDefinedTtlWrittenToSerializedJsonConfigFile(bool expectIsUserDef RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act @@ -300,10 +281,7 @@ public void CachePropertyNotWrittenToSerializedJsonConfigFile(string cacheConfig RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act @@ -342,10 +320,7 @@ public void DefaultTtlNotWrittenToSerializedJsonConfigFile(string cacheConfig) RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 2522806049..c0d24bd9d1 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -840,7 +840,7 @@ public void MsSqlConnStringSupplementedWithAppNameProperty( bool configParsed = RuntimeConfigLoader.TryParseConfig( runtimeConfig.ToJson(), out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + new ()); // Assert Assert.AreEqual( @@ -893,7 +893,7 @@ public void PgSqlConnStringSupplementedWithAppNameProperty( bool configParsed = RuntimeConfigLoader.TryParseConfig( runtimeConfig.ToJson(), out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + new ()); // Assert Assert.AreEqual( @@ -958,7 +958,7 @@ public void TestConnectionStringIsCorrectlyUpdatedWithApplicationName( bool configParsed = RuntimeConfigLoader.TryParseConfig( runtimeConfig.ToJson(), out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + new ()); // Assert Assert.AreEqual( @@ -2342,7 +2342,12 @@ public async Task TestSPRestDefaultsForManuallyConstructedConfigs( HttpStatusCode expectedResponseStatusCode) { string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson); - RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); + RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); string configFileName = "custom-config.json"; File.WriteAllText(configFileName, deserializedConfig.ToJson()); string[] args = new[] @@ -2425,7 +2430,12 @@ public async Task SanityTestForRestAndGQLRequestsWithoutMultipleMutationFeatureF // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the // configuration file (instead of using CLI). string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, BOOK_ENTITY_JSON); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); string configFileName = "custom-config.json"; File.WriteAllText(configFileName, deserializedConfig.ToJson()); string[] args = new[] @@ -3278,7 +3288,12 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr // The BASE_CONFIG omits the rest.request-body-strict option in the runtime section. string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson); - RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); + RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, deserializedConfig.ToJson()); string[] args = new[] @@ -3292,7 +3307,8 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(SupportedHttpVerb.Post); string requestBody = @"{ ""title"": ""Harry Potter and the Order of Phoenix"", - ""publisher_id"": 1234"; + ""publisher_id"": 1234 + }"; if (includeExtraneousFieldInRequestBody) { @@ -5459,7 +5475,7 @@ public static string GetConnectionStringFromEnvironmentConfig(string environment string sqlFile = new FileSystemRuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); string configPayload = File.ReadAllText(sqlFile); - RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replaceEnvVar: true); + RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replacementSettings: new()); return runtimeConfig.DataSource.ConnectionString; } diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 8d7dae0541..e200bcc3d9 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -10,7 +10,6 @@ using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -79,18 +78,18 @@ public void CheckConfigEnvParsingTest( if (replaceEnvVar) { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), "Should read the expected config"); } else { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), "Should read the expected config"); } Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)), "Should read actual config"); Assert.AreEqual(expectedConfig.ToJson(), actualConfig.ToJson()); } @@ -130,7 +129,7 @@ public void TestConfigParsingWithEnvVarReplacement(bool replaceEnvVar, string da string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceConfigForGivenDatabase(databaseType)); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replaceEnvVar: replaceEnvVar); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAKVVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -178,7 +177,7 @@ public void TestConfigParsingWhenDataSourceOptionsForCosmosDBContainsInvalidValu string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceOptionsForCosmosDBWithInvalidValues()); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replaceEnvVar: true); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -302,10 +301,7 @@ public void CheckConfigEnvParsingThrowExceptions(string invalidEnvVarName) { string json = @"{ ""foo"" : ""@env('envVarName'), @env('" + invalidEnvVarName + @"')"" }"; SetEnvVariables(); - StringJsonConverterFactory stringConverterFactory = new(EnvironmentVariableReplacementFailureMode.Throw); - JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; - options.Converters.Add(stringConverterFactory); - Assert.ThrowsException(() => JsonSerializer.Deserialize(json, options)); + Assert.ThrowsException(() => JsonSerializer.Deserialize(json)); } [DataRow("\"notsupporteddb\"", "", @@ -324,7 +320,7 @@ public void TestDataSourceDeserializationFailures(string dbType, string connecti ""entities"":{ } }"; // replaceEnvVar: true is needed to make sure we do post-processing for the connection string case - Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replaceEnvVar: true)); + Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAKVVar: true))); Assert.IsNull(deserializedConfig); } diff --git a/src/Service/Controllers/ConfigurationController.cs b/src/Service/Controllers/ConfigurationController.cs index be3f9bd727..a15a2f7577 100644 --- a/src/Service/Controllers/ConfigurationController.cs +++ b/src/Service/Controllers/ConfigurationController.cs @@ -91,8 +91,8 @@ public async Task Index([FromBody] ConfigurationPostParameters con configuration.Schema, configuration.ConnectionString, configuration.AccessToken, - replaceEnvVar: false, - replacementFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore); + replacementSettings: new(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAKVVar: false, envFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore) + ); if (initResult && _configurationProvider.TryGetConfig(out _)) {