Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-1012] Feature access using context #2764

Merged
merged 4 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ public static class AuthenticationSchemes
{
public const string BitwardenExternalCookieAuthenticationScheme = "bw.external";
}

public static class FeatureFlagKeys
{
public const string SecretsManager = "secrets-manager";
}
35 changes: 34 additions & 1 deletion src/Core/Services/IFeatureService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
namespace Bit.Core.Services;
using Bit.Core.Context;

namespace Bit.Core.Services;

public interface IFeatureService
{
/// <summary>
/// Checks whether online access to feature status is available.
/// </summary>
/// <returns>True if the service is online, otherwise false.</returns>
bool IsOnline();

/// <summary>
/// Checks whether a given feature is enabled.
/// </summary>
/// <param name="key">The key of the feature to check.</param>
/// <param name="currentContext">A context providing information that can be used to evaluate whether a feature should be on or off.</param>
/// <param name="defaultValue">The default value for the feature.</param>
/// <returns>True if the feature is enabled, otherwise false.</returns>
bool IsEnabled(string key, ICurrentContext currentContext, bool defaultValue = false);
withinfocus marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets the integer variation of a feature.
/// </summary>
/// <param name="key">The key of the feature to check.</param>
/// <param name="currentContext">A context providing information that can be used to evaluate the feature value.</param>
/// <param name="defaultValue">The default value for the feature.</param>
/// <returns>The feature variation value.</returns>
int GetIntVariation(string key, ICurrentContext currentContext, int defaultValue = 0);

/// <summary>
/// Gets the string variation of a feature.
/// </summary>
/// <param name="key">The key of the feature to check.</param>
/// <param name="currentContext">A context providing information that can be used to evaluate the feature value.</param>
/// <param name="defaultValue">The default value for the feature.</param>
/// <returns>The feature variation value.</returns>
string GetStringVariation(string key, ICurrentContext currentContext, string defaultValue = null);
}
39 changes: 38 additions & 1 deletion src/Core/Services/Implementations/LaunchDarklyFeatureService.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -47,8 +48,44 @@ public bool IsOnline()
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();
}
}
66 changes: 66 additions & 0 deletions test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,4 +27,68 @@ public void Offline_WhenSelfHost()

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<ICurrentContext>();
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<ICurrentContext>();
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<ICurrentContext>();
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<ICurrentContext>();
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<ICurrentContext>();
currentContext.UserId.Returns(Guid.NewGuid());

Assert.Null(sutProvider.Sut.GetStringVariation(FeatureFlagKeys.SecretsManager, currentContext));
}
}