Skip to content

Commit 49e775a

Browse files
authored
feat: support TLS connection in flagd provider (#48)
Signed-off-by: odubajDT <[email protected]>
1 parent f4d2142 commit 49e775a

File tree

5 files changed

+117
-9
lines changed

5 files changed

+117
-9
lines changed

Diff for: src/OpenFeature.Contrib.Providers.Flagd/FlagdConfig.cs

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ internal class FlagdConfig
88
internal const string EnvVarHost = "FLAGD_HOST";
99
internal const string EnvVarPort = "FLAGD_PORT";
1010
internal const string EnvVarTLS = "FLAGD_TLS";
11+
internal const string EnvCertPart = "FLAGD_SERVER_CERT_PATH";
1112
internal const string EnvVarSocketPath = "FLAGD_SOCKET_PATH";
1213
internal const string EnvVarCache = "FLAGD_CACHE";
1314
internal const string EnvVarMaxCacheSize = "FLAGD_MAX_CACHE_SIZE";
@@ -29,6 +30,17 @@ internal int MaxCacheSize
2930
get { return _maxCacheSize; }
3031
}
3132

33+
internal bool UseCertificate
34+
{
35+
get { return _cert.Length > 0; }
36+
}
37+
38+
internal string CertificatePath
39+
{
40+
get { return _cert; }
41+
set { _cert = value; }
42+
}
43+
3244
internal int MaxEventStreamRetries
3345
{
3446
get { return _maxEventStreamRetries; }
@@ -38,6 +50,7 @@ internal int MaxEventStreamRetries
3850
private string _host;
3951
private string _port;
4052
private bool _useTLS;
53+
private string _cert;
4154
private string _socketPath;
4255
private bool _cache;
4356
private int _maxCacheSize;
@@ -48,6 +61,7 @@ internal FlagdConfig()
4861
_host = Environment.GetEnvironmentVariable(EnvVarHost) ?? "localhost";
4962
_port = Environment.GetEnvironmentVariable(EnvVarPort) ?? "8013";
5063
_useTLS = bool.Parse(Environment.GetEnvironmentVariable(EnvVarTLS) ?? "false");
64+
_cert = Environment.GetEnvironmentVariable(EnvCertPart) ?? "";
5165
_socketPath = Environment.GetEnvironmentVariable(EnvVarSocketPath) ?? "";
5266
var cacheStr = Environment.GetEnvironmentVariable(EnvVarCache) ?? "";
5367

Diff for: src/OpenFeature.Contrib.Providers.Flagd/FlagdProvider.cs

+32-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
using System;
2+
using System.IO;
3+
using System.Text;
24
using System.Linq;
35
using System.Threading.Tasks;
6+
using System.Security.Cryptography.X509Certificates;
47

58
using Google.Protobuf.WellKnownTypes;
69
using Grpc.Core;
@@ -40,6 +43,7 @@ public sealed class FlagdProvider : FeatureProvider
4043
/// FLAGD_HOST - The host name of the flagd server (default="localhost")
4144
/// FLAGD_PORT - The port of the flagd server (default="8013")
4245
/// FLAGD_TLS - Determines whether to use https or not (default="false")
46+
/// FLAGD_FLAGD_SERVER_CERT_PATH - The path to the client certificate (default="")
4347
/// FLAGD_SOCKET_PATH - Path to the unix socket (default="")
4448
/// FLAGD_CACHE - Enable or disable the cache (default="false")
4549
/// FLAGD_MAX_CACHE_SIZE - The maximum size of the cache (default="10")
@@ -49,7 +53,7 @@ public FlagdProvider()
4953
{
5054
_config = new FlagdConfig();
5155

52-
_client = buildClientForPlatform(_config.GetUri());
56+
_client = BuildClientForPlatform(_config.GetUri());
5357

5458
_mtx = new System.Threading.Mutex();
5559

@@ -77,7 +81,7 @@ public FlagdProvider(Uri url)
7781

7882
_mtx = new System.Threading.Mutex();
7983

80-
_client = buildClientForPlatform(url);
84+
_client = BuildClientForPlatform(url);
8185
}
8286

8387

@@ -499,20 +503,41 @@ private static Value ConvertToPrimitiveValue(ProtoValue value)
499503
}
500504
}
501505

502-
private static Service.ServiceClient buildClientForPlatform(Uri url)
506+
private Service.ServiceClient BuildClientForPlatform(Uri url)
503507
{
504508
var useUnixSocket = url.ToString().StartsWith("unix://");
505509

506510
if (!useUnixSocket)
507511
{
508512
#if NET462
509-
return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions
513+
var handler = new WinHttpHandler();
514+
#else
515+
var handler = new HttpClientHandler();
516+
#endif
517+
if (_config.UseCertificate)
510518
{
511-
HttpHandler = new WinHttpHandler(),
512-
}));
519+
#if NET5_0_OR_GREATER
520+
if (File.Exists(_config.CertificatePath)) {
521+
X509Certificate2 certificate = new X509Certificate2(_config.CertificatePath);
522+
handler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) => {
523+
// the the custom cert to the chain, Build returns a bool if valid.
524+
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
525+
chain.ChainPolicy.CustomTrustStore.Add(certificate);
526+
return chain.Build(cert);
527+
};
528+
} else {
529+
throw new ArgumentException("Specified certificate cannot be found.");
530+
}
513531
#else
514-
return new Service.ServiceClient(GrpcChannel.ForAddress(url));
532+
// Pre-NET5.0 APIs for custom CA validation are cumbersome.
533+
// Looking for additional contributions here.
534+
throw new ArgumentException("Custom certificate authorities not supported on this platform.");
515535
#endif
536+
}
537+
return new Service.ServiceClient(GrpcChannel.ForAddress(url, new GrpcChannelOptions
538+
{
539+
HttpHandler = handler
540+
}));
516541
}
517542

518543
#if NET5_0_OR_GREATER

Diff for: src/OpenFeature.Contrib.Providers.Flagd/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,13 @@ The URI of the flagd server to which the `flagd Provider` connects to can either
8181
| host | FLAGD_HOST | string | localhost | |
8282
| port | FLAGD_PORT | number | 8013 | |
8383
| tls | FLAGD_TLS | boolean | false | |
84+
| tls certPath | FLAGD_SERVER_CERT_PATH | string | | |
8485
| unix socket path | FLAGD_SOCKET_PATH | string | | |
8586
| Caching | FLAGD_CACHE | string | | LRU |
8687
| Maximum cache size | FLAGD_MAX_CACHE_SIZE | number | 10 | |
8788
| Maximum event stream retries | FLAGD_MAX_EVENT_STREAM_RETRIES | number | 3 | |
8889

89-
Note that if `FLAGD_SOCKET_PATH` is set, this value takes precedence, and the other variables (`FLAGD_HOST`, `FLAGD_PORT`, `FLAGD_TLS`) are disregarded.
90+
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.
9091

9192

9293
If you rely on the environment variables listed above, you can use the empty constructor which then configures the provider accordingly:

Diff for: test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdConfigTest.cs

+20
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,32 @@ public void TestFlagdConfigEnabledCacheApplyCacheSize()
6161
Assert.Equal(20, config.MaxCacheSize);
6262
}
6363

64+
[Fact]
65+
public void TestFlagdConfigSetCertificatePath()
66+
{
67+
CleanEnvVars();
68+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvCertPart, "/cert/path");
69+
70+
var config = new FlagdConfig();
71+
72+
Assert.Equal("/cert/path", config.CertificatePath);
73+
Assert.True(config.UseCertificate);
74+
75+
CleanEnvVars();
76+
77+
config = new FlagdConfig();
78+
79+
Assert.Equal("", config.CertificatePath);
80+
Assert.False(config.UseCertificate);
81+
}
82+
6483
private void CleanEnvVars()
6584
{
6685
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarTLS, "");
6786
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarSocketPath, "");
6887
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarCache, "");
6988
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarMaxCacheSize, "");
89+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvCertPart, "");
7090
}
7191
}
7292
}

Diff for: test/OpenFeature.Contrib.Providers.Flagd.Test/FlagdProviderTest.cs

+49-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,62 @@
66
using OpenFeature.Error;
77
using ProtoValue = Google.Protobuf.WellKnownTypes.Value;
88
using System.Collections.Generic;
9-
using System.Linq;
109
using OpenFeature.Model;
1110
using System.Threading;
11+
using System;
1212

1313
namespace OpenFeature.Contrib.Providers.Flagd.Test
1414
{
1515
public class UnitTestFlagdProvider
1616
{
17+
[Fact]
18+
public void BuildClientForPlatform_Should_Throw_Exception_When_FlagdCertPath_Not_Exists()
19+
{
20+
// Arrange
21+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvCertPart, "non-existing-path");
22+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarHost, "localhost");
23+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarPort, "5001");
24+
25+
// Act & Assert
26+
Assert.Throws<ArgumentException>(() => new FlagdProvider());
27+
28+
// Cleanup
29+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvCertPart, "");
30+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarHost, "");
31+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarPort, "");
32+
}
33+
34+
[Fact]
35+
public void BuildClientForPlatform_Should_Return_Client_For_Non_Unix_Socket_Without_Certificate()
36+
{
37+
// Arrange
38+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarHost, "localhost");
39+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarPort, "5001");
40+
41+
// Act
42+
var flagdProvider = new FlagdProvider();
43+
var client = flagdProvider.GetClient();
44+
45+
// Assert
46+
Assert.NotNull(client);
47+
Assert.IsType<Service.ServiceClient>(client);
48+
49+
// Cleanup
50+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarHost, "");
51+
System.Environment.SetEnvironmentVariable(FlagdConfig.EnvVarPort, "");
52+
}
53+
54+
#if NET462
55+
[Fact]
56+
public void BuildClientForPlatform_Should_Throw_Exception_For_Unsupported_DotNet_Version()
57+
{
58+
// Arrange
59+
var url = new Uri("unix:///var/run/flagd.sock");
60+
61+
// Act & Assert
62+
Assert.Throws<Exception>(() => new FlagdProvider(url));
63+
}
64+
#endif
1765
[Fact]
1866
public void TestGetProviderName()
1967
{

0 commit comments

Comments
 (0)