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:
@@ -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