diff --git a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs index bcab5830..f83b5b33 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs +++ b/src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs @@ -1,6 +1,9 @@ using System; +using System.IO; +using System.Text; using System.Linq; using System.Threading.Tasks; +using System.Security.Cryptography.X509Certificates; using Google.Protobuf.WellKnownTypes; using Grpc.Core; @@ -387,17 +390,39 @@ private static Value ConvertToPrimitiveValue(ProtoValue value) private static Service.ServiceClient buildClientForPlatform(Uri url) { var useUnixSocket = url.ToString().StartsWith("unix://"); + X509Certificate2 certificate = null; if (!useUnixSocket) { + var flagdCertPath = Environment.GetEnvironmentVariable("FLAGD_SERVER_CERT_PATH") ?? ""; #if NET462 - return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions - { - HttpHandler = new WinHttpHandler() - })); + var handler = new WinHttpHandler(); +#else + var handler = new HttpClientHandler(); +#endif + if (flagdCertPath != "") { +#if NET5_0_OR_GREATER + if (File.Exists(flagdCertPath)) { + certificate = new X509Certificate2(flagdCertPath); + handler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) => { + // the the custom cert to the chain, Build returns a bool if valid. + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(certificate); + return chain.Build(cert); + }; + } else { + throw new ArgumentException("Specified certificate cannot be found."); + } #else - return new Service.ServiceClient(GrpcChannel.ForAddress(url)); + // Pre-NET5.0 APIs for custom CA validation are cumbersome. + // Looking for additional contributions here. + throw new ArgumentException("Custom certificate authorities not supported on this platform."); #endif + } + return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions + { + HttpHandler = handler + })); } #if NET5_0_OR_GREATER diff --git a/src/OpenFeature.Contrib.Providers.Flagd/README.md b/src/OpenFeature.Contrib.Providers.Flagd/README.md index 8351a0af..d922adbc 100644 --- a/src/OpenFeature.Contrib.Providers.Flagd/README.md +++ b/src/OpenFeature.Contrib.Providers.Flagd/README.md @@ -81,9 +81,10 @@ The URI of the flagd server to which the `flagd Provider` connects to can either | host | FLAGD_HOST | string | localhost | | | port | FLAGD_PORT | number | 8013 | | | tls | FLAGD_TLS | boolean | false | | +| tls certPath | FLAGD_SERVER_CERT_PATH | string | | | | unix socket path | FLAGD_SOCKET_PATH | string | | | -Note that if `FLAGD_SOCKET_PATH` is set, this value takes precedence, and the other variables (`FLAGD_HOST`, `FLAGD_PORT`, `FLAGD_TLS`) are disregarded. +Note that if `FLAGD_SOCKET_PATH` is set, this value takes precedence, and the other variables (`FLAGD_HOST`, `FLAGD_PORT`, `FLAGD_TLS`, `FLAGD_SERVER_CERT_PATH`) are disregarded. If you rely on the environment variables listed above, you can use the empty constructor which then configures the provider accordingly: diff --git a/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs b/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs index 7a62ee46..b9093166 100644 --- a/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs +++ b/test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs @@ -5,11 +5,55 @@ using Google.Protobuf.WellKnownTypes; using OpenFeature.Error; using ProtoValue = Google.Protobuf.WellKnownTypes.Value; +using System; namespace OpenFeature.Contrib.Providers.Flagd.Test { public class UnitTestFlagdProvider { + [Fact] + public void BuildClientForPlatform_Should_Return_Null_When_FlagdCertPath_Not_Exists() + { + // Arrange + var url = new Uri("https://localhost:5001"); + System.Environment.SetEnvironmentVariable("FLAGD_SERVER_CERT_PATH", "non-existing-path"); + + // Act + var flagdProvider = new FlagdProvider(url); + + // Assert + Assert.Null(flagdProvider.GetClient()); + + // Cleanup + System.Environment.SetEnvironmentVariable("FLAGD_SERVER_CERT_PATH", ""); + } + + [Fact] + public void BuildClientForPlatform_Should_Return_Client_For_Non_Unix_Socket_Without_Certificate() + { + // Arrange + var url = new Uri("https://localhost:5001"); + + // Act + var flagdProvider = new FlagdProvider(url); + var client = flagdProvider.GetClient(); + + // Assert + Assert.NotNull(client); + Assert.IsType(client); + } + +#if NET462 + [Fact] + public void BuildClientForPlatform_Should_Throw_Exception_For_Unsupported_DotNet_Version() + { + // Arrange + var url = new Uri("unix:///var/run/flagd.sock"); + + // Act & Assert + Assert.Throws(() => new FlagdProvider(url)); + } +#endif [Fact] public void TestGetProviderName() {