1
0
mirror of https://github.com/bitwarden/server synced 2025-12-15 15:53:59 +00:00

Add IPremiumAccessQuery interface and PremiumAccessQuery implementation for checking user premium access status

This commit is contained in:
Rui Tome
2025-12-04 17:13:24 +00:00
parent f83a79073c
commit d30f3bdbff
2 changed files with 147 additions and 0 deletions

View File

@@ -0,0 +1,50 @@
using Bit.Core.Entities;
namespace Bit.Core.Auth.UserFeatures.PremiumAccess;
/// <summary>
/// Query for checking premium access status for users.
/// This is the centralized location for determining if a user can access premium features
/// (either through personal subscription or organization membership).
///
/// <para>
/// <strong>Note:</strong> This is different from checking User.Premium, which only indicates
/// personal subscription status. Use these methods to check actual premium feature access.
/// </para>
/// </summary>
public interface IPremiumAccessQuery
{
/// <summary>
/// Checks if a user has access to premium features (personal subscription or organization).
/// This is the definitive way to check premium access for a single user.
/// </summary>
/// <param name="user">The user to check for premium access</param>
/// <returns>True if user can access premium features; false otherwise</returns>
Task<bool> CanAccessPremiumAsync(User user);
/// <summary>
/// Checks if a user has access to premium features (personal subscription or organization).
/// Use this overload when you already know the personal premium status and only need to check organization premium.
/// </summary>
/// <param name="userId">The user ID to check for premium access</param>
/// <param name="hasPersonalPremium">Whether the user has a personal premium subscription</param>
/// <returns>True if user can access premium features; false otherwise</returns>
Task<bool> CanAccessPremiumAsync(Guid userId, bool hasPersonalPremium);
/// <summary>
/// Checks if a user has access to premium features through organization membership only.
/// This is useful for determining the source of premium access (personal vs organization).
/// </summary>
/// <param name="userId">The user ID to check for organization premium access</param>
/// <returns>True if user has premium access through any organization; false otherwise</returns>
Task<bool> HasPremiumFromOrganizationAsync(Guid userId);
/// <summary>
/// Checks if multiple users have access to premium features (optimized bulk operation).
/// Uses cached organization abilities and minimizes database queries.
/// </summary>
/// <param name="users">The users to check for premium access</param>
/// <returns>Dictionary mapping user IDs to their premium access status (personal or through organization)</returns>
Task<Dictionary<Guid, bool>> CanAccessPremiumBulkAsync(IEnumerable<User> users);
}

View File

@@ -0,0 +1,97 @@
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.Auth.UserFeatures.PremiumAccess;
/// <summary>
/// Query for checking premium access status for users using cached organization abilities.
/// </summary>
public class PremiumAccessQuery : IPremiumAccessQuery
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IApplicationCacheService _applicationCacheService;
public PremiumAccessQuery(
IOrganizationUserRepository organizationUserRepository,
IApplicationCacheService applicationCacheService)
{
_organizationUserRepository = organizationUserRepository;
_applicationCacheService = applicationCacheService;
}
public async Task<bool> CanAccessPremiumAsync(User user)
{
return await CanAccessPremiumAsync(user.Id, user.Premium);
}
public async Task<bool> CanAccessPremiumAsync(Guid userId, bool hasPersonalPremium)
{
if (hasPersonalPremium)
{
return true;
}
return await HasPremiumFromOrganizationAsync(userId);
}
public async Task<bool> HasPremiumFromOrganizationAsync(Guid userId)
{
// Note: GetManyByUserAsync only returns Accepted and Confirmed status org users
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
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);
}
public async Task<Dictionary<Guid, bool>> CanAccessPremiumBulkAsync(IEnumerable<User> users)
{
var result = new Dictionary<Guid, bool>();
var usersList = users.ToList();
if (!usersList.Any())
{
return result;
}
var userIds = usersList.Select(u => u.Id).ToList();
// Get all org memberships for these users in one query
// Note: GetManyByManyUsersAsync only returns Accepted and Confirmed status org users
var allOrgUsers = await _organizationUserRepository.GetManyByManyUsersAsync(userIds);
var orgUsersGrouped = allOrgUsers
.Where(ou => ou.UserId.HasValue)
.GroupBy(ou => ou.UserId!.Value)
.ToDictionary(g => g.Key, g => g.ToList());
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
foreach (var user in usersList)
{
var hasPersonalPremium = user.Premium;
if (hasPersonalPremium)
{
result[user.Id] = true;
continue;
}
var hasPremiumFromOrg = orgUsersGrouped.TryGetValue(user.Id, out var userOrgs) &&
userOrgs.Any(ou =>
orgAbilities.TryGetValue(ou.OrganizationId, out var orgAbility) &&
orgAbility.UsersGetPremium &&
orgAbility.Enabled);
result[user.Id] = hasPremiumFromOrg;
}
return result;
}
}