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

Include Context information in config response #3916

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Api/Controllers/ConfigController.cs
Expand Up @@ -23,6 +23,6 @@ public class ConfigController : Controller
[HttpGet("")]
public ConfigResponseModel GetConfigs()
{
return new ConfigResponseModel(_globalSettings, _featureService.GetAll());
return new ConfigResponseModel(_globalSettings, _featureService.GetAll(), _featureService.GetFlagContext());
}
}
16 changes: 15 additions & 1 deletion src/Api/Models/Response/ConfigResponseModel.cs
@@ -1,4 +1,5 @@
using Bit.Core.Models.Api;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;

Expand All @@ -11,6 +12,7 @@ public class ConfigResponseModel : ResponseModel
public ServerConfigResponseModel Server { get; set; }
public EnvironmentConfigResponseModel Environment { get; set; }
public IDictionary<string, object> FeatureStates { get; set; }
public ContextResponseModel Context { get; set; }

public ConfigResponseModel() : base("config")
{
Expand All @@ -22,7 +24,7 @@ public ConfigResponseModel() : base("config")

public ConfigResponseModel(
IGlobalSettings globalSettings,
IDictionary<string, object> featureStates) : base("config")
IDictionary<string, object> featureStates, FeatureFlagContext featureFlagContext) : base("config")
{
Version = AssemblyHelpers.GetVersion();
GitHash = AssemblyHelpers.GetGitHash();
Expand All @@ -36,6 +38,7 @@ public ConfigResponseModel() : base("config")
Sso = globalSettings.BaseServiceUri.Sso
};
FeatureStates = featureStates;
Context = new ContextResponseModel(featureFlagContext.UserId, featureFlagContext.OrganizationIds);
}
}

Expand All @@ -54,3 +57,14 @@ public class EnvironmentConfigResponseModel
public string Notifications { get; set; }
public string Sso { get; set; }
}

public class ContextResponseModel
{
public Guid? UserId { get; set; }
public Guid[] OrganizationIds { get; set; }
public ContextResponseModel(Guid? userId, Guid[] organizationIds)
{
UserId = userId;
OrganizationIds = organizationIds;
}
}
7 changes: 7 additions & 0 deletions src/Core/Services/IFeatureService.cs
@@ -1,5 +1,11 @@
namespace Bit.Core.Services;

public struct FeatureFlagContext
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 I am still hung up on this being associated to feature flags. My opinion is that context is a platform-level concept and should be there and not in these classes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, I was thinking of context in terms of the LD context, but that's entirely derived from CurrentContext. A safe report of that context should fully describe anything that any given Feature Flag provider could use to key off of

{
public Guid? UserId { get; init; }
public Guid[] OrganizationIds { get; init; }
}

public interface IFeatureService
{
/// <summary>
Expand Down Expand Up @@ -37,4 +43,5 @@ public interface IFeatureService
/// </summary>
/// <returns>A dictionary of feature keys and their values.</returns>
Dictionary<string, object> GetAll();
FeatureFlagContext GetFlagContext();
}
Expand Up @@ -96,6 +96,15 @@ public string GetStringVariation(string key, string defaultValue = null)
return _client.StringVariation(key, BuildContext(), defaultValue);
}

public FeatureFlagContext GetFlagContext()
{
return new FeatureFlagContext()
{
UserId = _currentContext.UserId,
OrganizationIds = _currentContext.Organizations?.Select(o => o.Id).ToArray()
};
}

public Dictionary<string, object> GetAll()
{
var results = new Dictionary<string, object>();
Expand Down
58 changes: 50 additions & 8 deletions test/Core.Test/Services/LaunchDarklyFeatureServiceTests.cs
Expand Up @@ -29,22 +29,64 @@ private static SutProvider<LaunchDarklyFeatureService> GetSutProvider(IGlobalSet
return new SutProvider<LaunchDarklyFeatureService>(fixture)
.SetDependency(globalSettings)
.SetDependency(currentContext)
.SetDependency(client)
.Create();
.SetDependency(client);
}

[Fact]
public void NoContext_WhenUnauthed()
{
var sutProvider = GetSutProvider(new Settings.GlobalSettings());

var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(null as Guid?);
sutProvider.SetDependency(currentContext);

Assert.Equivalent(sutProvider.Create().Sut.GetFlagContext(), new FeatureFlagContext
{
UserId = null,
OrganizationIds = null,
});
}

[Fact]
public void UserContext_WhenAuthed()
{
var sutProvider = GetSutProvider(new Settings.GlobalSettings());

var userId = Guid.NewGuid();
var organizations = new List<CurrentContextOrganization>
{
new CurrentContextOrganization { Id = Guid.NewGuid() },
new CurrentContextOrganization { Id = Guid.NewGuid() },
};

var currentContext = Substitute.For<ICurrentContext>();
currentContext.UserId.Returns(userId);
currentContext.Organizations.Returns(organizations);
sutProvider.SetDependency(currentContext);

var expected = new FeatureFlagContext
{
UserId = userId,
OrganizationIds = organizations.Select(o => o.Id).ToArray(),
};

Assert.Equivalent(sutProvider.Create().Sut.GetFlagContext(), expected);

}

[Theory, BitAutoData]
public void DefaultFeatureValue_WhenSelfHost(string key)
{
var sutProvider = GetSutProvider(new Settings.GlobalSettings { SelfHosted = true });
var sutProvider = GetSutProvider(new Settings.GlobalSettings { SelfHosted = true }).Create();

Assert.False(sutProvider.Sut.IsEnabled(key));
}

[Fact]
public void DefaultFeatureValue_NoSdkKey()
{
var sutProvider = GetSutProvider(new Settings.GlobalSettings());
var sutProvider = GetSutProvider(new Settings.GlobalSettings()).Create();

Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey));
}
Expand All @@ -54,7 +96,7 @@ public void FeatureValue_Boolean()
{
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };

var sutProvider = GetSutProvider(settings);
var sutProvider = GetSutProvider(settings).Create();

Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey));
}
Expand All @@ -64,7 +106,7 @@ public void FeatureValue_Int()
{
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };

var sutProvider = GetSutProvider(settings);
var sutProvider = GetSutProvider(settings).Create();

Assert.Equal(0, sutProvider.Sut.GetIntVariation(_fakeFeatureKey));
}
Expand All @@ -74,15 +116,15 @@ public void FeatureValue_String()
{
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };

var sutProvider = GetSutProvider(settings);
var sutProvider = GetSutProvider(settings).Create();

Assert.Null(sutProvider.Sut.GetStringVariation(_fakeFeatureKey));
}

[Fact(Skip = "For local development")]
public void GetAll()
{
var sutProvider = GetSutProvider(new Settings.GlobalSettings());
var sutProvider = GetSutProvider(new Settings.GlobalSettings()).Create();

var results = sutProvider.Sut.GetAll();

Expand Down