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