mirror of
https://github.com/bitwarden/server
synced 2025-12-15 07:43:54 +00:00
Add IPremiumAccessQuery interface and PremiumAccessQuery implementation for checking user premium access status
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user