diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 3a7b65443..19117f1b7 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -22,3 +22,8 @@ public static class AuthenticationSchemes
{
public const string BitwardenExternalCookieAuthenticationScheme = "bw.external";
}
+
+public static class FeatureFlagKeys
+{
+ public const string SecretsManager = "secrets-manager";
+}
diff --git a/src/Core/Services/IFeatureService.cs b/src/Core/Services/IFeatureService.cs
index 956eab917..0d8e7a422 100644
--- a/src/Core/Services/IFeatureService.cs
+++ b/src/Core/Services/IFeatureService.cs
@@ -1,6 +1,39 @@
-namespace Bit.Core.Services;
+using Bit.Core.Context;
+
+namespace Bit.Core.Services;
public interface IFeatureService
{
+ ///
+ /// Checks whether online access to feature status is available.
+ ///
+ /// True if the service is online, otherwise false.
bool IsOnline();
+
+ ///
+ /// Checks whether a given feature is enabled.
+ ///
+ /// The key of the feature to check.
+ /// A context providing information that can be used to evaluate whether a feature should be on or off.
+ /// The default value for the feature.
+ /// True if the feature is enabled, otherwise false.
+ bool IsEnabled(string key, ICurrentContext currentContext, bool defaultValue = false);
+
+ ///
+ /// Gets the integer variation of a feature.
+ ///
+ /// The key of the feature to check.
+ /// A context providing information that can be used to evaluate the feature value.
+ /// The default value for the feature.
+ /// The feature variation value.
+ int GetIntVariation(string key, ICurrentContext currentContext, int defaultValue = 0);
+
+ ///
+ /// Gets the string variation of a feature.
+ ///
+ /// The key of the feature to check.
+ /// A context providing information that can be used to evaluate the feature value.
+ /// The default value for the feature.
+ /// The feature variation value.
+ string GetStringVariation(string key, ICurrentContext currentContext, string defaultValue = null);
}
diff --git a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs
index d69a91681..eeb2e5723 100644
--- a/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs
+++ b/src/Core/Services/Implementations/LaunchDarklyFeatureService.cs
@@ -1,4 +1,5 @@
-using Bit.Core.Settings;
+using Bit.Core.Context;
+using Bit.Core.Settings;
using LaunchDarkly.Sdk.Server;
using LaunchDarkly.Sdk.Server.Integrations;
@@ -47,8 +48,44 @@ public class LaunchDarklyFeatureService : IFeatureService, IDisposable
return _client.Initialized && !_client.IsOffline();
}
+ public bool IsEnabled(string key, ICurrentContext currentContext, bool defaultValue = false)
+ {
+ return _client.BoolVariation(key, BuildContext(currentContext), defaultValue);
+ }
+
+ public int GetIntVariation(string key, ICurrentContext currentContext, int defaultValue = 0)
+ {
+ return _client.IntVariation(key, BuildContext(currentContext), defaultValue);
+ }
+
+ public string GetStringVariation(string key, ICurrentContext currentContext, string defaultValue = null)
+ {
+ return _client.StringVariation(key, BuildContext(currentContext), defaultValue);
+ }
+
public void Dispose()
{
_client?.Dispose();
}
+
+ private LaunchDarkly.Sdk.Context BuildContext(ICurrentContext currentContext)
+ {
+ var builder = LaunchDarkly.Sdk.Context.MultiBuilder();
+
+ if (currentContext.UserId.HasValue)
+ {
+ var user = LaunchDarkly.Sdk.Context.Builder(currentContext.UserId.Value.ToString());
+ user.Kind(LaunchDarkly.Sdk.ContextKind.Default);
+ builder.Add(user.Build());
+ }
+
+ if (currentContext.OrganizationId.HasValue)
+ {
+ var org = LaunchDarkly.Sdk.Context.Builder(currentContext.OrganizationId.Value.ToString());
+ org.Kind("org");
+ builder.Add(org.Build());
+ }
+
+ return builder.Build();
+ }
}
diff --git a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs
index 1491f41aa..efc84bd78 100644
--- a/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs
+++ b/test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs
@@ -1,8 +1,10 @@
using AutoFixture;
+using Bit.Core.Context;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
@@ -25,4 +27,68 @@ public class LaunchDarklyFeatureServiceTests
Assert.False(sutProvider.Sut.IsOnline());
}
+
+ [Theory, BitAutoData]
+ public void DefaultFeatureValue_WhenSelfHost(string key)
+ {
+ var sutProvider = GetSutProvider(new Core.Settings.GlobalSettings() { SelfHosted = true });
+
+ var currentContext = Substitute.For();
+ currentContext.UserId.Returns(Guid.NewGuid());
+
+ Assert.False(sutProvider.Sut.IsEnabled(key, currentContext));
+ }
+
+ [Fact]
+ public void DefaultFeatureValue_NoSdkKey()
+ {
+ var sutProvider = GetSutProvider(new Core.Settings.GlobalSettings());
+
+ var currentContext = Substitute.For();
+ currentContext.UserId.Returns(Guid.NewGuid());
+
+ Assert.False(sutProvider.Sut.IsEnabled(FeatureFlagKeys.SecretsManager, currentContext));
+ }
+
+ [Fact(Skip = "For local development")]
+ public void FeatureValue_Boolean()
+ {
+ var settings = new Core.Settings.GlobalSettings();
+ settings.LaunchDarkly.SdkKey = "somevalue";
+
+ var sutProvider = GetSutProvider(settings);
+
+ var currentContext = Substitute.For();
+ currentContext.UserId.Returns(Guid.NewGuid());
+
+ Assert.False(sutProvider.Sut.IsEnabled(FeatureFlagKeys.SecretsManager, currentContext));
+ }
+
+ [Fact(Skip = "For local development")]
+ public void FeatureValue_Int()
+ {
+ var settings = new Core.Settings.GlobalSettings();
+ settings.LaunchDarkly.SdkKey = "somevalue";
+
+ var sutProvider = GetSutProvider(settings);
+
+ var currentContext = Substitute.For();
+ currentContext.UserId.Returns(Guid.NewGuid());
+
+ Assert.Equal(0, sutProvider.Sut.GetIntVariation(FeatureFlagKeys.SecretsManager, currentContext));
+ }
+
+ [Fact(Skip = "For local development")]
+ public void FeatureValue_String()
+ {
+ var settings = new Core.Settings.GlobalSettings();
+ settings.LaunchDarkly.SdkKey = "somevalue";
+
+ var sutProvider = GetSutProvider(settings);
+
+ var currentContext = Substitute.For();
+ currentContext.UserId.Returns(Guid.NewGuid());
+
+ Assert.Null(sutProvider.Sut.GetStringVariation(FeatureFlagKeys.SecretsManager, currentContext));
+ }
}