1
0
mirror of https://github.com/bitwarden/server synced 2026-02-21 03:43:44 +00:00

[PM-29498] Remove Premium Feature Flagged Logic (#6967)

* Remove feature flag logic and fix unit tests

* Simplify query

* Fix test

* Fix local review
This commit is contained in:
sven-bitwarden
2026-02-19 08:06:17 -06:00
committed by GitHub
parent cfd5bedae0
commit 4d91350fb7
5 changed files with 60 additions and 312 deletions

View File

@@ -8,7 +8,6 @@ using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations;
@@ -16,108 +15,16 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
{
private readonly IUserRepository _userRepository;
private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;
private readonly IFeatureService _featureService;
public TwoFactorIsEnabledQuery(
IUserRepository userRepository,
IHasPremiumAccessQuery hasPremiumAccessQuery,
IFeatureService featureService)
IHasPremiumAccessQuery hasPremiumAccessQuery)
{
_userRepository = userRepository;
_hasPremiumAccessQuery = hasPremiumAccessQuery;
_featureService = featureService;
}
public async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync(IEnumerable<Guid> userIds)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
{
return await TwoFactorIsEnabledVNextAsync(userIds);
}
var result = new List<(Guid userId, bool hasTwoFactor)>();
if (userIds == null || !userIds.Any())
{
return result;
}
var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]);
foreach (var userDetail in userDetails)
{
result.Add(
(userDetail.Id,
await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(),
() => Task.FromResult(userDetail.HasPremiumAccess))
)
);
}
return result;
}
public async Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser
{
if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
{
return await TwoFactorIsEnabledVNextAsync(users);
}
var userIds = users
.Select(u => u.GetUserId())
.Where(u => u.HasValue)
.Select(u => u.Value)
.ToList();
var twoFactorResults = await TwoFactorIsEnabledAsync(userIds);
var result = new List<(T user, bool twoFactorIsEnabled)>();
foreach (var user in users)
{
var userId = user.GetUserId();
if (userId.HasValue)
{
var hasTwoFactor = twoFactorResults.FirstOrDefault(res => res.userId == userId.Value).twoFactorIsEnabled;
result.Add((user, hasTwoFactor));
}
else
{
result.Add((user, false));
}
}
return result;
}
public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)
{
var userId = user.GetUserId();
if (!userId.HasValue)
{
return false;
}
if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
{
var userEntity = user as User ?? await _userRepository.GetByIdAsync(userId.Value);
if (userEntity == null)
{
throw new NotFoundException();
}
return await TwoFactorIsEnabledVNextAsync(userEntity);
}
return await TwoFactorEnabledAsync(
user.GetTwoFactorProviders(),
async () =>
{
var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value);
return calcUser?.HasPremiumAccess ?? false;
});
}
private async Task<IEnumerable<(Guid userId, bool twoFactorIsEnabled)>> TwoFactorIsEnabledVNextAsync(IEnumerable<Guid> userIds)
{
var result = new List<(Guid userId, bool hasTwoFactor)>();
if (userIds == null || !userIds.Any())
@@ -158,8 +65,7 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
return result;
}
private async Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledVNextAsync<T>(IEnumerable<T> users)
where T : ITwoFactorProvidersUser
public async Task<IEnumerable<(T user, bool twoFactorIsEnabled)>> TwoFactorIsEnabledAsync<T>(IEnumerable<T> users) where T : ITwoFactorProvidersUser
{
var userIds = users
.Select(u => u.GetUserId())
@@ -167,7 +73,7 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
.Select(u => u.Value)
.ToList();
var twoFactorResults = await TwoFactorIsEnabledVNextAsync(userIds);
var twoFactorResults = await TwoFactorIsEnabledAsync(userIds);
var result = new List<(T user, bool twoFactorIsEnabled)>();
@@ -188,10 +94,21 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
return result;
}
private async Task<bool> TwoFactorIsEnabledVNextAsync(User user)
public async Task<bool> TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user)
{
var enabledProviders = GetEnabledTwoFactorProviders(user);
var userId = user.GetUserId();
if (!userId.HasValue)
{
return false;
}
var userEntity = user as User ?? await _userRepository.GetByIdAsync(userId.Value);
if (userEntity == null)
{
throw new NotFoundException();
}
var enabledProviders = GetEnabledTwoFactorProviders(userEntity);
if (!enabledProviders.Any())
{
return false;
@@ -200,7 +117,7 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
// If all providers require premium, check if user has premium access
if (enabledProviders.All(TwoFactorProvider.RequiresPremium))
{
return await _hasPremiumAccessQuery.HasPremiumAccessAsync(user.Id);
return await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId.Value);
}
// User has at least one non-premium provider
@@ -226,48 +143,4 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
where provider.Value?.Enabled ?? false
select provider.Key).ToList();
}
/// <summary>
/// Checks to see what kind of two-factor is enabled.
/// We use a delegate to check if the user has premium access, since there are multiple ways to
/// determine if a user has premium access.
/// </summary>
/// <param name="providers">dictionary of two factor providers</param>
/// <param name="hasPremiumAccessDelegate">function to check if the user has premium access</param>
/// <returns> true if the user has two factor enabled; false otherwise;</returns>
private async static Task<bool> TwoFactorEnabledAsync(
Dictionary<TwoFactorProviderType, TwoFactorProvider> providers,
Func<Task<bool>> hasPremiumAccessDelegate)
{
// If there are no providers, then two factor is not enabled
if (providers == null || providers.Count == 0)
{
return false;
}
// Get all enabled providers
// TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.
var enabledProviderKeys = from provider in providers
where provider.Value?.Enabled ?? false
select provider.Key;
// If no providers are enabled then two factor is not enabled
if (!enabledProviderKeys.Any())
{
return false;
}
// If there are only premium two factor options then standard two factor is not enabled
var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium);
if (onlyHasPremiumTwoFactor)
{
// There are no Standard two factor options, check if the user has premium access
// If the user has premium access, then two factor is enabled
var premiumAccess = await hasPremiumAccessDelegate();
return premiumAccess;
}
// The user has at least one non-premium two factor option
return true;
}
}

View File

@@ -142,7 +142,7 @@ public static class FeatureFlagKeys
public const string ScimRevokeV2 = "pm-32394-scim-revoke-put-v2";
public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache";
public const string DefaultUserCollectionRestore = "pm-30883-my-items-restored-users";
public const string PremiumAccessQuery = "pm-29495-refactor-premium-interface";
public const string RefactorMembersComponent = "pm-29503-refactor-members-inheritance";
public const string BulkReinviteUI = "pm-28416-bulk-reinvite-ux-improvements";
public const string UpdateJoinOrganizationEmailTemplate = "pm-28396-update-join-organization-email-template";

View File

@@ -922,12 +922,7 @@ public class UserService : UserManager<User>, IUserService
return false;
}
if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
{
return user.Premium || await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);
}
return user.Premium || await HasPremiumFromOrganization(user);
return user.Premium || await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);
}
public async Task<bool> HasPremiumFromOrganization(User user)
@@ -938,25 +933,7 @@ public class UserService : UserManager<User>, IUserService
return false;
}
if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
{
return await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);
}
// orgUsers in the Invited status are not associated with a userId yet, so this will get
// orgUsers in Accepted and Confirmed states only
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(userId.Value);
if (!orgUsers.Any())
{
return false;
}
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
return orgUsers.Any(ou =>
orgAbilities.TryGetValue(ou.OrganizationId, out var orgAbility) &&
orgAbility.UsersGetPremium &&
orgAbility.Enabled);
return await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);
}
public async Task<string> GenerateSignInTokenAsync(User user, string purpose)