diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs
index 00bac01f76..00ba706a41 100644
--- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs
+++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs
@@ -20,6 +20,12 @@ public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser, I
public string Email { get; set; }
public string AvatarColor { get; set; }
public string TwoFactorProviders { get; set; }
+ ///
+ /// Indicates whether the user has a personal premium subscription.
+ /// Does not include premium access from organizations -
+ /// do not use this to check whether the user can access premium features.
+ /// Null when the organization user is in Invited status (UserId is null).
+ ///
public bool? Premium { get; set; }
public OrganizationUserStatusType Status { get; set; }
public OrganizationUserType Type { get; set; }
diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs
index cc86d3d71d..e6c0c1444a 100644
--- a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs
+++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs
@@ -4,16 +4,37 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
+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;
-public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery
+public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery
{
- private readonly IUserRepository _userRepository = userRepository;
+ private readonly IUserRepository _userRepository;
+ private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;
+ private readonly IFeatureService _featureService;
+
+ public TwoFactorIsEnabledQuery(
+ IUserRepository userRepository,
+ IHasPremiumAccessQuery hasPremiumAccessQuery,
+ IFeatureService featureService)
+ {
+ _userRepository = userRepository;
+ _hasPremiumAccessQuery = hasPremiumAccessQuery;
+ _featureService = featureService;
+ }
public async Task> TwoFactorIsEnabledAsync(IEnumerable userIds)
{
+ if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
+ {
+ return await TwoFactorIsEnabledVNextAsync(userIds);
+ }
+
var result = new List<(Guid userId, bool hasTwoFactor)>();
if (userIds == null || !userIds.Any())
{
@@ -36,6 +57,11 @@ public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFacto
public async Task> TwoFactorIsEnabledAsync(IEnumerable users) where T : ITwoFactorProvidersUser
{
+ if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
+ {
+ return await TwoFactorIsEnabledVNextAsync(users);
+ }
+
var userIds = users
.Select(u => u.GetUserId())
.Where(u => u.HasValue)
@@ -71,13 +97,134 @@ public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFacto
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;
- });
+ user.GetTwoFactorProviders(),
+ async () =>
+ {
+ var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value);
+ return calcUser?.HasPremiumAccess ?? false;
+ });
+ }
+
+ private async Task> TwoFactorIsEnabledVNextAsync(IEnumerable userIds)
+ {
+ var result = new List<(Guid userId, bool hasTwoFactor)>();
+ if (userIds == null || !userIds.Any())
+ {
+ return result;
+ }
+
+ var users = await _userRepository.GetManyAsync([.. userIds]);
+
+ // Get enabled providers for each user
+ var usersTwoFactorProvidersMap = users.ToDictionary(u => u.Id, GetEnabledTwoFactorProviders);
+
+ // Bulk fetch premium status only for users who need it (those with only premium providers)
+ var userIdsNeedingPremium = usersTwoFactorProvidersMap
+ .Where(kvp => kvp.Value.Any() && kvp.Value.All(TwoFactorProvider.RequiresPremium))
+ .Select(kvp => kvp.Key)
+ .ToList();
+
+ var premiumStatusMap = userIdsNeedingPremium.Count > 0
+ ? await _hasPremiumAccessQuery.HasPremiumAccessAsync(userIdsNeedingPremium)
+ : new Dictionary();
+
+ foreach (var user in users)
+ {
+ var userTwoFactorProviders = usersTwoFactorProvidersMap[user.Id];
+
+ if (!userTwoFactorProviders.Any())
+ {
+ result.Add((user.Id, false));
+ continue;
+ }
+
+ // User has providers. If they're in the premium check map, verify premium status
+ var twoFactorIsEnabled = !premiumStatusMap.TryGetValue(user.Id, out var hasPremium) || hasPremium;
+ result.Add((user.Id, twoFactorIsEnabled));
+ }
+
+ return result;
+ }
+
+ private async Task> TwoFactorIsEnabledVNextAsync(IEnumerable users)
+ where T : ITwoFactorProvidersUser
+ {
+ var userIds = users
+ .Select(u => u.GetUserId())
+ .Where(u => u.HasValue)
+ .Select(u => u.Value)
+ .ToList();
+
+ var twoFactorResults = await TwoFactorIsEnabledVNextAsync(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;
+ }
+
+ private async Task TwoFactorIsEnabledVNextAsync(User user)
+ {
+ var enabledProviders = GetEnabledTwoFactorProviders(user);
+
+ if (!enabledProviders.Any())
+ {
+ return false;
+ }
+
+ // If all providers require premium, check if user has premium access
+ if (enabledProviders.All(TwoFactorProvider.RequiresPremium))
+ {
+ return await _hasPremiumAccessQuery.HasPremiumAccessAsync(user.Id);
+ }
+
+ // User has at least one non-premium provider
+ return true;
+ }
+
+ ///
+ /// Gets all enabled two-factor provider types for a user.
+ ///
+ /// user with two factor providers
+ /// list of enabled provider types
+ private static IList GetEnabledTwoFactorProviders(User user)
+ {
+ var providers = user.GetTwoFactorProviders();
+
+ if (providers == null || providers.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ // TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.
+ return (from provider in providers
+ where provider.Value?.Enabled ?? false
+ select provider.Key).ToList();
}
///
diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
index d6593f5365..5ceefed603 100644
--- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
@@ -6,6 +6,7 @@ using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Payment;
using Bit.Core.Billing.Premium.Commands;
+using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
@@ -31,6 +32,7 @@ public static class ServiceCollectionExtensions
services.AddPaymentOperations();
services.AddOrganizationLicenseCommandsQueries();
services.AddPremiumCommands();
+ services.AddPremiumQueries();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -50,4 +52,9 @@ public static class ServiceCollectionExtensions
services.AddScoped();
services.AddTransient();
}
+
+ private static void AddPremiumQueries(this IServiceCollection services)
+ {
+ services.AddScoped();
+ }
}
diff --git a/src/Core/Billing/Premium/Models/UserPremiumAccess.cs b/src/Core/Billing/Premium/Models/UserPremiumAccess.cs
new file mode 100644
index 0000000000..639d175d25
--- /dev/null
+++ b/src/Core/Billing/Premium/Models/UserPremiumAccess.cs
@@ -0,0 +1,29 @@
+namespace Bit.Core.Billing.Premium.Models;
+
+///
+/// Represents user premium access status from personal subscriptions and organization memberships.
+///
+public class UserPremiumAccess
+{
+ ///
+ /// The unique identifier for the user.
+ ///
+ public Guid Id { get; set; }
+
+ ///
+ /// Indicates whether the user has a personal premium subscription.
+ /// This does NOT include premium access from organizations.
+ ///
+ public bool PersonalPremium { get; set; }
+
+ ///
+ /// Indicates whether the user has premium access through any organization membership.
+ /// This is true if the user is a member of at least one enabled organization that grants premium access to users.
+ ///
+ public bool OrganizationPremium { get; set; }
+
+ ///
+ /// Indicates whether the user has premium access from any source (personal subscription or organization).
+ ///
+ public bool HasPremiumAccess => PersonalPremium || OrganizationPremium;
+}
diff --git a/src/Core/Billing/Premium/Queries/HasPremiumAccessQuery.cs b/src/Core/Billing/Premium/Queries/HasPremiumAccessQuery.cs
new file mode 100644
index 0000000000..e90710a9b3
--- /dev/null
+++ b/src/Core/Billing/Premium/Queries/HasPremiumAccessQuery.cs
@@ -0,0 +1,49 @@
+using Bit.Core.Exceptions;
+using Bit.Core.Repositories;
+
+namespace Bit.Core.Billing.Premium.Queries;
+
+public class HasPremiumAccessQuery : IHasPremiumAccessQuery
+{
+ private readonly IUserRepository _userRepository;
+
+ public HasPremiumAccessQuery(IUserRepository userRepository)
+ {
+ _userRepository = userRepository;
+ }
+
+ public async Task HasPremiumAccessAsync(Guid userId)
+ {
+ var user = await _userRepository.GetPremiumAccessAsync(userId);
+ if (user == null)
+ {
+ throw new NotFoundException();
+ }
+
+ return user.HasPremiumAccess;
+ }
+
+ public async Task> HasPremiumAccessAsync(IEnumerable userIds)
+ {
+ var distinctUserIds = userIds.Distinct().ToList();
+ var usersWithPremium = await _userRepository.GetPremiumAccessByIdsAsync(distinctUserIds);
+
+ if (usersWithPremium.Count() != distinctUserIds.Count)
+ {
+ throw new NotFoundException();
+ }
+
+ return usersWithPremium.ToDictionary(u => u.Id, u => u.HasPremiumAccess);
+ }
+
+ public async Task HasPremiumFromOrganizationAsync(Guid userId)
+ {
+ var user = await _userRepository.GetPremiumAccessAsync(userId);
+ if (user == null)
+ {
+ throw new NotFoundException();
+ }
+
+ return user.OrganizationPremium;
+ }
+}
diff --git a/src/Core/Billing/Premium/Queries/IHasPremiumAccessQuery.cs b/src/Core/Billing/Premium/Queries/IHasPremiumAccessQuery.cs
new file mode 100644
index 0000000000..e5545b1ade
--- /dev/null
+++ b/src/Core/Billing/Premium/Queries/IHasPremiumAccessQuery.cs
@@ -0,0 +1,30 @@
+namespace Bit.Core.Billing.Premium.Queries;
+
+///
+/// Centralized query for checking if users have premium access through personal subscriptions or organizations.
+/// Note: Different from User.Premium which only checks personal subscriptions.
+///
+public interface IHasPremiumAccessQuery
+{
+ ///
+ /// Checks if a user has premium access (personal or organization).
+ ///
+ /// The user ID to check
+ /// True if user can access premium features
+ Task HasPremiumAccessAsync(Guid userId);
+
+ ///
+ /// Checks premium access for multiple users.
+ ///
+ /// The user IDs to check
+ /// Dictionary mapping user IDs to their premium access status
+ Task> HasPremiumAccessAsync(IEnumerable userIds);
+
+ ///
+ /// Checks if a user belongs to any organization that grants premium (enabled org with UsersGetPremium).
+ /// Returns true regardless of personal subscription. Useful for UI decisions like showing subscription options.
+ ///
+ /// The user ID to check
+ /// True if user is in any organization that grants premium
+ Task HasPremiumFromOrganizationAsync(Guid userId);
+}
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index cf3f40ec80..95ab009722 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -143,6 +143,7 @@ public static class FeatureFlagKeys
public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration";
public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud";
public const string BulkRevokeUsersV2 = "pm-28456-bulk-revoke-users-v2";
+ public const string PremiumAccessQuery = "pm-21411-premium-access-query";
/* Architecture */
public const string DesktopMigrationMilestone1 = "desktop-ui-migration-milestone-1";
diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs
index 1ca6606779..669e32bcbe 100644
--- a/src/Core/Entities/User.cs
+++ b/src/Core/Entities/User.cs
@@ -69,6 +69,11 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac
/// The security state is a signed object attesting to the version of the user's account.
///
public string? SecurityState { get; set; }
+ ///
+ /// Indicates whether the user has a personal premium subscription.
+ /// Does not include premium access from organizations -
+ /// do not use this to check whether the user can access premium features.
+ ///
public bool Premium { get; set; }
public DateTime? PremiumExpirationDate { get; set; }
public DateTime? RenewalReminderDate { get; set; }
diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs
index 7cdd159224..47ddb86f8e 100644
--- a/src/Core/Repositories/IUserRepository.cs
+++ b/src/Core/Repositories/IUserRepository.cs
@@ -1,4 +1,5 @@
-using Bit.Core.Entities;
+using Bit.Core.Billing.Premium.Models;
+using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;
@@ -24,6 +25,7 @@ public interface IUserRepository : IRepository
/// Retrieves the data for the requested user IDs and includes an additional property indicating
/// whether the user has premium access directly or through an organization.
///
+ [Obsolete("Use GetPremiumAccessByIdsAsync instead. This method will be removed in a future version.")]
Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids);
///
/// Retrieves the data for the requested user ID and includes additional property indicating
@@ -34,8 +36,23 @@ public interface IUserRepository : IRepository
///
/// The user ID to retrieve data for.
/// User data with calculated premium access; null if nothing is found
+ [Obsolete("Use GetPremiumAccessAsync instead. This method will be removed in a future version.")]
Task GetCalculatedPremiumAsync(Guid userId);
///
+ /// Retrieves premium access status for multiple users.
+ /// For internal use - consumers should use IHasPremiumAccessQuery instead.
+ ///
+ /// The user IDs to check
+ /// Collection of UserPremiumAccess objects containing premium status information
+ Task> GetPremiumAccessByIdsAsync(IEnumerable ids);
+ ///
+ /// Retrieves premium access status for a single user.
+ /// For internal use - consumers should use IHasPremiumAccessQuery instead.
+ ///
+ /// The user ID to check
+ /// UserPremiumAccess object containing premium status information, or null if user not found
+ Task GetPremiumAccessAsync(Guid userId);
+ ///
/// Sets a new user key and updates all encrypted data.
/// Warning: Any user key encrypted data not included will be lost.
///
diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs
index 0506e08cfc..fade63de51 100644
--- a/src/Core/Services/IUserService.cs
+++ b/src/Core/Services/IUserService.cs
@@ -60,7 +60,7 @@ public interface IUserService
///
/// Checks if the user has access to premium features, either through a personal subscription or through an organization.
///
- /// This is the preferred way to definitively know if a user has access to premium features.
+ /// This is the preferred way to definitively know if a user has access to premium features when you already have the User object.
///
/// user being acted on
/// true if they can access premium; false otherwise.
@@ -74,6 +74,7 @@ public interface IUserService
///
/// user being acted on
/// true if they can access premium because of organization membership; false otherwise.
+ [Obsolete("Use IHasPremiumAccessQuery.HasPremiumFromOrganizationAsync instead. This method will be removed in a future version.")]
Task HasPremiumFromOrganization(User user);
Task GenerateSignInTokenAsync(User user, string purpose);
diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs
index fbc382cb08..8db66211b1 100644
--- a/src/Core/Services/Implementations/UserService.cs
+++ b/src/Core/Services/Implementations/UserService.cs
@@ -17,6 +17,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Models.Sales;
+using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
@@ -73,6 +74,7 @@ public class UserService : UserManager, IUserService
private readonly IDistributedCache _distributedCache;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IPricingClient _pricingClient;
+ private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery;
public UserService(
IUserRepository userRepository,
@@ -108,7 +110,8 @@ public class UserService : UserManager, IUserService
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IDistributedCache distributedCache,
IPolicyRequirementQuery policyRequirementQuery,
- IPricingClient pricingClient)
+ IPricingClient pricingClient,
+ IHasPremiumAccessQuery hasPremiumAccessQuery)
: base(
store,
optionsAccessor,
@@ -149,6 +152,7 @@ public class UserService : UserManager, IUserService
_distributedCache = distributedCache;
_policyRequirementQuery = policyRequirementQuery;
_pricingClient = pricingClient;
+ _hasPremiumAccessQuery = hasPremiumAccessQuery;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
@@ -1112,6 +1116,11 @@ public class UserService : UserManager, IUserService
return false;
}
+ if (_featureService.IsEnabled(FeatureFlagKeys.PremiumAccessQuery))
+ {
+ return user.Premium || await _hasPremiumAccessQuery.HasPremiumFromOrganizationAsync(userId.Value);
+ }
+
return user.Premium || await HasPremiumFromOrganization(user);
}
@@ -1123,6 +1132,11 @@ public class UserService : UserManager, 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);
diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs
index 86ab063a5f..224351f034 100644
--- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs
+++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs
@@ -1,6 +1,7 @@
using System.Data;
using System.Text.Json;
using Bit.Core;
+using Bit.Core.Billing.Premium.Models;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
@@ -381,6 +382,25 @@ public class UserRepository : Repository, IUserRepository
return result.SingleOrDefault();
}
+ public async Task> GetPremiumAccessByIdsAsync(IEnumerable ids)
+ {
+ using (var connection = new SqlConnection(ReadOnlyConnectionString))
+ {
+ var results = await connection.QueryAsync(
+ $"[{Schema}].[{Table}_ReadPremiumAccessByIds]",
+ new { Ids = ids.ToGuidIdArrayTVP() },
+ commandType: CommandType.StoredProcedure);
+
+ return results.ToList();
+ }
+ }
+
+ public async Task GetPremiumAccessAsync(Guid userId)
+ {
+ var result = await GetPremiumAccessByIdsAsync([userId]);
+ return result.SingleOrDefault();
+ }
+
private async Task ProtectDataAndSaveAsync(User user, Func saveTask)
{
if (user == null)
diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs
index 47369f5e3d..93d8fe2d7d 100644
--- a/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs
+++ b/src/Infrastructure.EntityFramework/AdminConsole/Configurations/OrganizationEntityTypeConfiguration.cs
@@ -18,7 +18,7 @@ public class OrganizationEntityTypeConfiguration : IEntityTypeConfiguration new { o.Id, o.Enabled }),
- o => o.UseTotp);
+ o => new { o.UseTotp, o.UsersGetPremium });
builder.ToTable(nameof(Organization));
}
diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs
index a43c692be3..9bf093e506 100644
--- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs
@@ -1,4 +1,5 @@
using AutoMapper;
+using Bit.Core.Billing.Premium.Models;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Data;
@@ -350,6 +351,36 @@ public class UserRepository : Repository, IUserR
return result.FirstOrDefault();
}
+ public async Task> GetPremiumAccessByIdsAsync(IEnumerable ids)
+ {
+ using (var scope = ServiceScopeFactory.CreateScope())
+ {
+ var dbContext = GetDatabaseContext(scope);
+
+ var users = await dbContext.Users
+ .Where(x => ids.Contains(x.Id))
+ .Include(u => u.OrganizationUsers)
+ .ThenInclude(ou => ou.Organization)
+ .ToListAsync();
+
+ return users.Select(user => new UserPremiumAccess
+ {
+ Id = user.Id,
+ PersonalPremium = user.Premium,
+ OrganizationPremium = user.OrganizationUsers
+ .Any(ou => ou.Organization != null &&
+ ou.Organization.Enabled == true &&
+ ou.Organization.UsersGetPremium == true)
+ }).ToList();
+ }
+ }
+
+ public async Task GetPremiumAccessAsync(Guid userId)
+ {
+ var result = await GetPremiumAccessByIdsAsync([userId]);
+ return result.FirstOrDefault();
+ }
+
public override async Task DeleteAsync(Core.Entities.User user)
{
using (var scope = ServiceScopeFactory.CreateScope())
diff --git a/src/Sql/dbo/Stored Procedures/User_ReadPremiumAccessByIds.sql b/src/Sql/dbo/Stored Procedures/User_ReadPremiumAccessByIds.sql
new file mode 100644
index 0000000000..a4c73c39df
--- /dev/null
+++ b/src/Sql/dbo/Stored Procedures/User_ReadPremiumAccessByIds.sql
@@ -0,0 +1,15 @@
+CREATE PROCEDURE [dbo].[User_ReadPremiumAccessByIds]
+ @Ids [dbo].[GuidIdArray] READONLY
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ SELECT
+ UPA.[Id],
+ UPA.[PersonalPremium],
+ UPA.[OrganizationPremium]
+ FROM
+ [dbo].[UserPremiumAccessView] UPA
+ WHERE
+ UPA.[Id] IN (SELECT [Id] FROM @Ids)
+END
diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql
index d8635c8ac9..f07cd4ce0d 100644
--- a/src/Sql/dbo/Tables/Organization.sql
+++ b/src/Sql/dbo/Tables/Organization.sql
@@ -69,7 +69,7 @@ CREATE TABLE [dbo].[Organization] (
GO
CREATE NONCLUSTERED INDEX [IX_Organization_Enabled]
ON [dbo].[Organization]([Id] ASC, [Enabled] ASC)
- INCLUDE ([UseTotp]);
+ INCLUDE ([UseTotp], [UsersGetPremium]);
GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_Organization_Identifier]
diff --git a/src/Sql/dbo/Views/UserPremiumAccessView.sql b/src/Sql/dbo/Views/UserPremiumAccessView.sql
new file mode 100644
index 0000000000..a20cab8fb3
--- /dev/null
+++ b/src/Sql/dbo/Views/UserPremiumAccessView.sql
@@ -0,0 +1,21 @@
+CREATE VIEW [dbo].[UserPremiumAccessView]
+AS
+SELECT
+ U.[Id],
+ U.[Premium] AS [PersonalPremium],
+ CAST(
+ MAX(CASE
+ WHEN O.[Id] IS NOT NULL THEN 1
+ ELSE 0
+ END) AS BIT
+ ) AS [OrganizationPremium]
+FROM
+ [dbo].[User] U
+LEFT JOIN
+ [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]
+LEFT JOIN
+ [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
+ AND O.[UsersGetPremium] = 1
+ AND O.[Enabled] = 1
+GROUP BY
+ U.[Id], U.[Premium];
diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs
index adeac45d06..3a98fb44fb 100644
--- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs
+++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs
@@ -1,10 +1,13 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth;
+using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Entities;
+using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
+using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@@ -404,6 +407,277 @@ public class TwoFactorIsEnabledQueryTests
.GetCalculatedPremiumAsync(default);
}
+ [Theory]
+ [BitAutoData((IEnumerable)null)]
+ [BitAutoData([])]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_WithNoUserIds_ReturnsEmpty(
+ IEnumerable userIds,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Theory]
+ [BitAutoData(TwoFactorProviderType.Duo)]
+ [BitAutoData(TwoFactorProviderType.YubiKey)]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_WithMixedScenarios_ReturnsCorrectResults(
+ TwoFactorProviderType premiumProviderType,
+ SutProvider sutProvider,
+ User user1,
+ User user2,
+ User user3)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ var users = new List { user1, user2, user3 };
+ var userIds = users.Select(u => u.Id).ToList();
+
+ // User 1: Non-premium provider → 2FA enabled
+ user1.SetTwoFactorProviders(new Dictionary
+ {
+ { TwoFactorProviderType.Authenticator, new TwoFactorProvider { Enabled = true } }
+ });
+
+ // User 2: Premium provider + has premium → 2FA enabled
+ user2.SetTwoFactorProviders(new Dictionary
+ {
+ { premiumProviderType, new TwoFactorProvider { Enabled = true } }
+ });
+
+ // User 3: Premium provider + no premium → 2FA disabled
+ user3.SetTwoFactorProviders(new Dictionary
+ {
+ { premiumProviderType, new TwoFactorProvider { Enabled = true } }
+ });
+
+ var premiumStatus = new Dictionary
+ {
+ { user2.Id, true },
+ { user3.Id, false }
+ };
+
+ sutProvider.GetDependency()
+ .GetManyAsync(Arg.Is>(ids => ids.SequenceEqual(userIds)))
+ .Returns(users);
+
+ sutProvider.GetDependency()
+ .HasPremiumAccessAsync(Arg.Is>(ids =>
+ ids.Count() == 2 && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
+ .Returns(premiumStatus);
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
+
+ // Assert
+ Assert.Contains(result, res => res.userId == user1.Id && res.twoFactorIsEnabled == true); // Non-premium provider
+ Assert.Contains(result, res => res.userId == user2.Id && res.twoFactorIsEnabled == true); // Premium + has premium
+ Assert.Contains(result, res => res.userId == user3.Id && res.twoFactorIsEnabled == false); // Premium + no premium
+ }
+
+ [Theory]
+ [BitAutoData(TwoFactorProviderType.Duo)]
+ [BitAutoData(TwoFactorProviderType.YubiKey)]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_OnlyChecksPremiumAccessForUsersWhoNeedIt(
+ TwoFactorProviderType premiumProviderType,
+ SutProvider sutProvider,
+ User user1,
+ User user2,
+ User user3)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ var users = new List { user1, user2, user3 };
+ var userIds = users.Select(u => u.Id).ToList();
+
+ // User 1: Has non-premium provider - should NOT trigger premium check
+ user1.SetTwoFactorProviders(new Dictionary
+ {
+ { TwoFactorProviderType.Authenticator, new TwoFactorProvider { Enabled = true } }
+ });
+
+ // User 2 & 3: Have only premium providers - SHOULD trigger premium check
+ user2.SetTwoFactorProviders(new Dictionary
+ {
+ { premiumProviderType, new TwoFactorProvider { Enabled = true } }
+ });
+ user3.SetTwoFactorProviders(new Dictionary
+ {
+ { premiumProviderType, new TwoFactorProvider { Enabled = true } }
+ });
+
+ var premiumStatus = new Dictionary
+ {
+ { user2.Id, true },
+ { user3.Id, false }
+ };
+
+ sutProvider.GetDependency()
+ .GetManyAsync(Arg.Is>(ids => ids.SequenceEqual(userIds)))
+ .Returns(users);
+
+ sutProvider.GetDependency()
+ .HasPremiumAccessAsync(Arg.Is>(ids =>
+ ids.Count() == 2 && ids.Contains(user2.Id) && ids.Contains(user3.Id)))
+ .Returns(premiumStatus);
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds);
+
+ // Assert - Verify optimization: premium checked ONLY for users 2 and 3 (not user 1)
+ await sutProvider.GetDependency()
+ .Received(1)
+ .HasPremiumAccessAsync(Arg.Is>(ids =>
+ ids.Count() == 2 && ids.Contains(user2.Id) && ids.Contains(user3.Id)));
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_WithNoUserIds_ReturnsAllTwoFactorDisabled(
+ SutProvider sutProvider,
+ List users)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ foreach (var user in users)
+ {
+ user.UserId = null;
+ }
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(users);
+
+ // Assert
+ foreach (var user in users)
+ {
+ Assert.Contains(result, res => res.user.Equals(user) && res.twoFactorIsEnabled == false);
+ }
+
+ // No UserIds were supplied so no calls to the UserRepository should have been made
+ await sutProvider.GetDependency()
+ .DidNotReceiveWithAnyArgs()
+ .GetManyAsync(default);
+ }
+
+ [Theory]
+ [BitAutoData(TwoFactorProviderType.Authenticator, true)] // Non-premium provider
+ [BitAutoData(TwoFactorProviderType.Duo, true)] // Premium provider with premium access
+ [BitAutoData(TwoFactorProviderType.YubiKey, false)] // Premium provider without premium access
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_SingleUser_VariousScenarios(
+ TwoFactorProviderType providerType,
+ bool hasPremiumAccess,
+ SutProvider sutProvider,
+ User user)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ user.SetTwoFactorProviders(new Dictionary
+ {
+ { providerType, new TwoFactorProvider { Enabled = true } }
+ });
+
+ sutProvider.GetDependency()
+ .HasPremiumAccessAsync(user.Id)
+ .Returns(hasPremiumAccess);
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
+
+ // Assert
+ var requiresPremium = TwoFactorProvider.RequiresPremium(providerType);
+ var expectedResult = !requiresPremium || hasPremiumAccess;
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_WithNoEnabledProviders_ReturnsFalse(
+ SutProvider sutProvider,
+ User user)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ user.SetTwoFactorProviders(new Dictionary
+ {
+ { TwoFactorProviderType.Email, new TwoFactorProvider { Enabled = false } }
+ });
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_WithNullProviders_ReturnsFalse(
+ SutProvider sutProvider,
+ User user)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ user.TwoFactorProviders = null;
+
+ // Act
+ var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task TwoFactorIsEnabledAsync_WhenPremiumAccessQueryEnabled_UserNotFound_ThrowsNotFoundException(
+ SutProvider sutProvider,
+ Guid userId)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .IsEnabled(FeatureFlagKeys.PremiumAccessQuery)
+ .Returns(true);
+
+ var testUser = new TestTwoFactorProviderUser
+ {
+ Id = userId,
+ TwoFactorProviders = null
+ };
+
+ sutProvider.GetDependency()
+ .GetByIdAsync(userId)
+ .Returns((User)null);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ async () => await sutProvider.Sut.TwoFactorIsEnabledAsync(testUser));
+ }
+
private class TestTwoFactorProviderUser : ITwoFactorProvidersUser
{
public Guid? Id { get; set; }
@@ -418,10 +692,5 @@ public class TwoFactorIsEnabledQueryTests
{
return Id;
}
-
- public bool GetPremium()
- {
- return Premium;
- }
}
}
diff --git a/test/Core.Test/Billing/Premium/Queries/HasPremiumAccessQueryTests.cs b/test/Core.Test/Billing/Premium/Queries/HasPremiumAccessQueryTests.cs
new file mode 100644
index 0000000000..31547dffbe
--- /dev/null
+++ b/test/Core.Test/Billing/Premium/Queries/HasPremiumAccessQueryTests.cs
@@ -0,0 +1,234 @@
+using Bit.Core.Billing.Premium.Models;
+using Bit.Core.Billing.Premium.Queries;
+using Bit.Core.Exceptions;
+using Bit.Core.Repositories;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
+using Xunit;
+
+namespace Bit.Core.Test.Billing.Premium.Queries;
+
+[SutProviderCustomize]
+public class HasPremiumAccessQueryTests
+{
+ [Theory, BitAutoData]
+ public async Task HasPremiumAccessAsync_WhenUserHasPersonalPremium_ReturnsTrue(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = true;
+ user.OrganizationPremium = false;
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumAccessAsync_WhenUserHasNoPersonalPremiumButHasOrgPremium_ReturnsTrue(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = false;
+ user.OrganizationPremium = true; // Has org premium
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumAccessAsync_WhenUserHasNoPersonalPremiumAndNoOrgPremium_ReturnsFalse(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = false;
+ user.OrganizationPremium = false;
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumAccessAsync_WhenUserNotFound_ThrowsNotFoundException(
+ Guid userId,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(userId)
+ .Returns((UserPremiumAccess?)null);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.HasPremiumAccessAsync(userId));
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumFromOrganizationAsync_WhenUserHasNoOrganizations_ReturnsFalse(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = false;
+ user.OrganizationPremium = false; // No premium from anywhere
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);
+
+ // Assert
+ Assert.False(result);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumFromOrganizationAsync_WhenUserHasPremiumFromOrg_ReturnsTrue(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = false; // No personal premium
+ user.OrganizationPremium = true; // But has premium from org
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);
+
+ // Assert
+ Assert.True(result);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumFromOrganizationAsync_WhenUserHasOnlyPersonalPremium_ReturnsFalse(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = true; // Has personal premium
+ user.OrganizationPremium = false; // Not in any org that grants premium
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);
+
+ // Assert
+ Assert.False(result); // Should return false because user is not in an org that grants premium
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumFromOrganizationAsync_WhenUserHasBothPersonalAndOrgPremium_ReturnsTrue(
+ UserPremiumAccess user,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user.PersonalPremium = true; // Has personal premium
+ user.OrganizationPremium = true; // Also in an org that grants premium
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(user.Id)
+ .Returns(user);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);
+
+ // Assert
+ Assert.True(result); // Should return true because user IS in an org that grants premium (regardless of personal premium)
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumFromOrganizationAsync_WhenUserNotFound_ThrowsNotFoundException(
+ Guid userId,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ sutProvider.GetDependency()
+ .GetPremiumAccessAsync(userId)
+ .Returns((UserPremiumAccess?)null);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.HasPremiumFromOrganizationAsync(userId));
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumAccessAsync_Bulk_WhenEmptyList_ReturnsEmptyDictionary(
+ SutProvider sutProvider)
+ {
+ // Arrange
+ var userIds = new List();
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessByIdsAsync(userIds)
+ .Returns(new List());
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumAccessAsync(userIds);
+
+ // Assert
+ Assert.Empty(result);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HasPremiumAccessAsync_Bulk_ReturnsCorrectStatus(
+ UserPremiumAccess user1,
+ UserPremiumAccess user2,
+ UserPremiumAccess user3,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ user1.PersonalPremium = true;
+ user1.OrganizationPremium = false;
+ user2.PersonalPremium = false;
+ user2.OrganizationPremium = false;
+ user3.PersonalPremium = false;
+ user3.OrganizationPremium = true;
+
+ var users = new List { user1, user2, user3 };
+ var userIds = users.Select(u => u.Id).ToList();
+
+ sutProvider.GetDependency()
+ .GetPremiumAccessByIdsAsync(Arg.Is>(ids => ids.SequenceEqual(userIds)))
+ .Returns(users);
+
+ // Act
+ var result = await sutProvider.Sut.HasPremiumAccessAsync(userIds);
+
+ // Assert
+ Assert.Equal(3, result.Count);
+ Assert.True(result[user1.Id]); // Personal premium
+ Assert.False(result[user2.Id]); // No premium
+ Assert.True(result[user3.Id]); // Organization premium
+ }
+}
diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs
index dd84df07be..bbbd6d5cdb 100644
--- a/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs
+++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/UserRepositoryTests.cs
@@ -179,4 +179,325 @@ public class UserRepositoryTests
Assert.Equal(CollectionType.SharedCollection, updatedCollection2.Type);
Assert.Equal(user2.Email, updatedCollection2.DefaultUserCollectionEmail);
}
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_WithPersonalPremium_ReturnsCorrectAccess(
+ IUserRepository userRepository)
+ {
+ // Arrange
+ var user = await userRepository.CreateAsync(new User
+ {
+ Name = "Premium User",
+ Email = $"premium+{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = true
+ });
+
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.True(result.PersonalPremium);
+ Assert.False(result.OrganizationPremium);
+ Assert.True(result.HasPremiumAccess);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_WithOrganizationPremium_ReturnsCorrectAccess(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository)
+ {
+ // Arrange
+ var user = await userRepository.CreateAsync(new User
+ {
+ Name = "Org User",
+ Email = $"org+{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var organization = await organizationRepository.CreateTestOrganizationAsync();
+ await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
+
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.False(result.PersonalPremium);
+ Assert.True(result.OrganizationPremium);
+ Assert.True(result.HasPremiumAccess);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_WithDisabledOrganization_ReturnsNoOrganizationPremium(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository)
+ {
+ // Arrange
+ var user = await userRepository.CreateAsync(new User
+ {
+ Name = "User",
+ Email = $"user+{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var organization = await organizationRepository.CreateTestOrganizationAsync();
+ organization.Enabled = false;
+ await organizationRepository.ReplaceAsync(organization);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
+
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.False(result.OrganizationPremium);
+ Assert.False(result.HasPremiumAccess);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_WithOrganizationUsersGetPremiumFalse_ReturnsNoOrganizationPremium(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository)
+ {
+ // Arrange
+ var user = await userRepository.CreateAsync(new User
+ {
+ Name = "User",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var organization = await organizationRepository.CreateTestOrganizationAsync();
+ organization.UsersGetPremium = false;
+ await organizationRepository.ReplaceAsync(organization);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
+
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.False(result.OrganizationPremium);
+ Assert.False(result.HasPremiumAccess);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_WithMultipleOrganizations_OneProvidesPremium_ReturnsOrganizationPremium(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository)
+ {
+ // Arrange
+ var user = await userRepository.CreateAsync(new User
+ {
+ Name = "User With Premium Org",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var orgWithPremium = await organizationRepository.CreateTestOrganizationAsync();
+ await organizationUserRepository.CreateTestOrganizationUserAsync(orgWithPremium, user);
+
+ var orgNoPremium = await organizationRepository.CreateTestOrganizationAsync();
+ orgNoPremium.UsersGetPremium = false;
+ await organizationRepository.ReplaceAsync(orgNoPremium);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(orgNoPremium, user);
+
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.False(result.PersonalPremium);
+ Assert.True(result.OrganizationPremium);
+ Assert.True(result.HasPremiumAccess);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_WithMultipleOrganizations_NoneProvidePremium_ReturnsNoOrganizationPremium(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository)
+ {
+ // Arrange
+ var user = await userRepository.CreateAsync(new User
+ {
+ Name = "User With No Premium Orgs",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var disabledOrg = await organizationRepository.CreateTestOrganizationAsync();
+ disabledOrg.Enabled = false;
+ await organizationRepository.ReplaceAsync(disabledOrg);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(disabledOrg, user);
+
+ var orgNoPremium = await organizationRepository.CreateTestOrganizationAsync();
+ orgNoPremium.UsersGetPremium = false;
+ await organizationRepository.ReplaceAsync(orgNoPremium);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(orgNoPremium, user);
+
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(user.Id);
+
+ // Assert
+ Assert.NotNull(result);
+ Assert.False(result.PersonalPremium);
+ Assert.False(result.OrganizationPremium);
+ Assert.False(result.HasPremiumAccess);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessAsync_NonExistentUser_ReturnsNull(
+ IUserRepository userRepository)
+ {
+ // Act
+ var result = await userRepository.GetPremiumAccessAsync(Guid.NewGuid());
+
+ // Assert
+ Assert.Null(result);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessByIdsAsync_MultipleUsers_ReturnsCorrectAccessForEach(
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository,
+ IOrganizationUserRepository organizationUserRepository)
+ {
+ // Arrange
+ var personalPremiumUser = await userRepository.CreateAsync(new User
+ {
+ Name = "Personal Premium",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = true
+ });
+
+ var orgPremiumUser = await userRepository.CreateAsync(new User
+ {
+ Name = "Org Premium",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var bothPremiumUser = await userRepository.CreateAsync(new User
+ {
+ Name = "Both Premium",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = true
+ });
+
+ var noPremiumUser = await userRepository.CreateAsync(new User
+ {
+ Name = "No Premium",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var multiOrgUser = await userRepository.CreateAsync(new User
+ {
+ Name = "Multi Org User",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = false
+ });
+
+ var personalPremiumWithDisabledOrg = await userRepository.CreateAsync(new User
+ {
+ Name = "Personal Premium With Disabled Org",
+ Email = $"{Guid.NewGuid()}@example.com",
+ ApiKey = "TEST",
+ SecurityStamp = "stamp",
+ Premium = true
+ });
+
+ var organization = await organizationRepository.CreateTestOrganizationAsync();
+ await organizationUserRepository.CreateTestOrganizationUserAsync(organization, orgPremiumUser);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(organization, bothPremiumUser);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(organization, multiOrgUser);
+
+ var orgWithoutPremium = await organizationRepository.CreateTestOrganizationAsync();
+ orgWithoutPremium.UsersGetPremium = false;
+ await organizationRepository.ReplaceAsync(orgWithoutPremium);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(orgWithoutPremium, multiOrgUser);
+
+ var disabledOrg = await organizationRepository.CreateTestOrganizationAsync();
+ disabledOrg.Enabled = false;
+ await organizationRepository.ReplaceAsync(disabledOrg);
+ await organizationUserRepository.CreateTestOrganizationUserAsync(disabledOrg, personalPremiumWithDisabledOrg);
+
+ // Act
+ var results = await userRepository.GetPremiumAccessByIdsAsync([
+ personalPremiumUser.Id,
+ orgPremiumUser.Id,
+ bothPremiumUser.Id,
+ noPremiumUser.Id,
+ multiOrgUser.Id,
+ personalPremiumWithDisabledOrg.Id
+ ]);
+
+ var resultsList = results.ToList();
+
+ // Assert
+ Assert.Equal(6, resultsList.Count);
+
+ var personalResult = resultsList.First(r => r.Id == personalPremiumUser.Id);
+ Assert.True(personalResult.PersonalPremium);
+ Assert.False(personalResult.OrganizationPremium);
+
+ var orgResult = resultsList.First(r => r.Id == orgPremiumUser.Id);
+ Assert.False(orgResult.PersonalPremium);
+ Assert.True(orgResult.OrganizationPremium);
+
+ var bothResult = resultsList.First(r => r.Id == bothPremiumUser.Id);
+ Assert.True(bothResult.PersonalPremium);
+ Assert.True(bothResult.OrganizationPremium);
+
+ var noneResult = resultsList.First(r => r.Id == noPremiumUser.Id);
+ Assert.False(noneResult.PersonalPremium);
+ Assert.False(noneResult.OrganizationPremium);
+
+ var multiResult = resultsList.First(r => r.Id == multiOrgUser.Id);
+ Assert.False(multiResult.PersonalPremium);
+ Assert.True(multiResult.OrganizationPremium);
+
+ var personalWithDisabledOrgResult = resultsList.First(r => r.Id == personalPremiumWithDisabledOrg.Id);
+ Assert.True(personalWithDisabledOrgResult.PersonalPremium);
+ Assert.False(personalWithDisabledOrgResult.OrganizationPremium);
+ }
+
+ [Theory, DatabaseData]
+ public async Task GetPremiumAccessByIdsAsync_EmptyList_ReturnsEmptyResult(
+ IUserRepository userRepository)
+ {
+ // Act
+ var results = await userRepository.GetPremiumAccessByIdsAsync([]);
+
+ // Assert
+ Assert.Empty(results);
+ }
}
diff --git a/util/Migrator/DbScripts/2025-12-12_00_UserPremiumAccessView.sql b/util/Migrator/DbScripts/2025-12-12_00_UserPremiumAccessView.sql
new file mode 100644
index 0000000000..b467f29acc
--- /dev/null
+++ b/util/Migrator/DbScripts/2025-12-12_00_UserPremiumAccessView.sql
@@ -0,0 +1,60 @@
+-- Add UsersGetPremium to IX_Organization_Enabled index to support premium access queries
+
+IF EXISTS (
+ SELECT * FROM sys.indexes
+ WHERE name = 'IX_Organization_Enabled'
+ AND object_id = OBJECT_ID('[dbo].[Organization]')
+)
+BEGIN
+ CREATE NONCLUSTERED INDEX [IX_Organization_Enabled]
+ ON [dbo].[Organization]([Id] ASC, [Enabled] ASC)
+ INCLUDE ([UseTotp], [UsersGetPremium])
+ WITH (DROP_EXISTING = ON);
+END
+ELSE
+BEGIN
+ CREATE NONCLUSTERED INDEX [IX_Organization_Enabled]
+ ON [dbo].[Organization]([Id] ASC, [Enabled] ASC)
+ INCLUDE ([UseTotp], [UsersGetPremium]);
+END
+GO
+
+CREATE OR ALTER VIEW [dbo].[UserPremiumAccessView]
+AS
+SELECT
+ U.[Id],
+ U.[Premium] AS [PersonalPremium],
+ CAST(
+ MAX(CASE
+ WHEN O.[Id] IS NOT NULL THEN 1
+ ELSE 0
+ END) AS BIT
+ ) AS [OrganizationPremium]
+FROM
+ [dbo].[User] U
+LEFT JOIN
+ [dbo].[OrganizationUser] OU ON OU.[UserId] = U.[Id]
+LEFT JOIN
+ [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId]
+ AND O.[UsersGetPremium] = 1
+ AND O.[Enabled] = 1
+GROUP BY
+ U.[Id], U.[Premium];
+GO
+
+CREATE OR ALTER PROCEDURE [dbo].[User_ReadPremiumAccessByIds]
+ @Ids [dbo].[GuidIdArray] READONLY
+AS
+BEGIN
+ SET NOCOUNT ON
+
+ SELECT
+ UPA.[Id],
+ UPA.[PersonalPremium],
+ UPA.[OrganizationPremium]
+ FROM
+ [dbo].[UserPremiumAccessView] UPA
+ WHERE
+ UPA.[Id] IN (SELECT [Id] FROM @Ids)
+END
+GO
diff --git a/util/MySqlMigrations/Migrations/20251212171212_OrganizationUsersGetPremiumIndex.Designer.cs b/util/MySqlMigrations/Migrations/20251212171212_OrganizationUsersGetPremiumIndex.Designer.cs
new file mode 100644
index 0000000000..72bdc1fb41
--- /dev/null
+++ b/util/MySqlMigrations/Migrations/20251212171212_OrganizationUsersGetPremiumIndex.Designer.cs
@@ -0,0 +1,3443 @@
+//
+using System;
+using Bit.Infrastructure.EntityFramework.Repositories;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Bit.MySqlMigrations.Migrations
+{
+ [DbContext(typeof(DatabaseContext))]
+ [Migration("20251212171212_OrganizationUsersGetPremiumIndex")]
+ partial class OrganizationUsersGetPremiumIndex
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.8")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b =>
+ {
+ b.Property("CipherId")
+ .HasColumnType("char(36)");
+
+ b.Property("CollectionId")
+ .HasColumnType("char(36)");
+
+ b.Property("CollectionName")
+ .HasColumnType("longtext");
+
+ b.Property("Email")
+ .HasColumnType("longtext");
+
+ b.Property("GroupId")
+ .HasColumnType("char(36)");
+
+ b.Property("GroupName")
+ .HasColumnType("longtext");
+
+ b.Property("HidePasswords")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Manage")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ReadOnly")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ResetPasswordKey")
+ .HasColumnType("longtext");
+
+ b.Property("TwoFactorProviders")
+ .HasColumnType("longtext");
+
+ b.Property("UserGuid")
+ .HasColumnType("char(36)");
+
+ b.Property("UserName")
+ .HasColumnType("longtext");
+
+ b.Property("UsesKeyConnector")
+ .HasColumnType("tinyint(1)");
+
+ b.ToTable("OrganizationMemberBaseDetails");
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AllowAdminAccessToAllCollectionItems")
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true);
+
+ b.Property("BillingEmail")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("varchar(256)");
+
+ b.Property("BusinessAddress1")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessAddress2")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessAddress3")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessCountry")
+ .HasMaxLength(2)
+ .HasColumnType("varchar(2)");
+
+ b.Property("BusinessName")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BusinessTaxNumber")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ExpirationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Gateway")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("GatewayCustomerId")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("GatewaySubscriptionId")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Identifier")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("LicenseKey")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("LimitCollectionCreation")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LimitCollectionDeletion")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LimitItemDeletion")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("MaxAutoscaleSeats")
+ .HasColumnType("int");
+
+ b.Property("MaxAutoscaleSmSeats")
+ .HasColumnType("int");
+
+ b.Property("MaxAutoscaleSmServiceAccounts")
+ .HasColumnType("int");
+
+ b.Property("MaxCollections")
+ .HasColumnType("smallint");
+
+ b.Property("MaxStorageGb")
+ .HasColumnType("smallint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("OwnersNotifiedOfAutoscaling")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Plan")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("PlanType")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("PrivateKey")
+ .HasColumnType("longtext");
+
+ b.Property("PublicKey")
+ .HasColumnType("longtext");
+
+ b.Property("ReferenceData")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Seats")
+ .HasColumnType("int");
+
+ b.Property("SelfHost")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("SmSeats")
+ .HasColumnType("int");
+
+ b.Property("SmServiceAccounts")
+ .HasColumnType("int");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Storage")
+ .HasColumnType("bigint");
+
+ b.Property("SyncSeats")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("TwoFactorProviders")
+ .HasColumnType("longtext");
+
+ b.Property("Use2fa")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseAdminSponsoredFamilies")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseApi")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseAutomaticUserConfirmation")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseCustomPermissions")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseDirectory")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseEvents")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseGroups")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseKeyConnector")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseOrganizationDomains")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsePasswordManager")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsePhishingBlocker")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsePolicies")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseResetPassword")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseRiskInsights")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseScim")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseSecretsManager")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseSso")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UseTotp")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("UsersGetPremium")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Id", "Enabled")
+ .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" });
+
+ b.ToTable("Organization", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("Configuration")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("OrganizationId", "Type")
+ .IsUnique()
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.ToTable("OrganizationIntegration", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("Configuration")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EventType")
+ .HasColumnType("int");
+
+ b.Property("Filters")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationIntegrationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Template")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationIntegrationId");
+
+ b.ToTable("OrganizationIntegrationConfiguration", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Data")
+ .HasColumnType("longtext");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId")
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.HasIndex("OrganizationId", "Type")
+ .IsUnique()
+ .HasAnnotation("SqlServer:Clustered", false);
+
+ b.ToTable("Policy", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("BillingEmail")
+ .HasColumnType("longtext");
+
+ b.Property("BillingPhone")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress1")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress2")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessAddress3")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessCountry")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessName")
+ .HasColumnType("longtext");
+
+ b.Property("BusinessTaxNumber")
+ .HasColumnType("longtext");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DiscountId")
+ .HasColumnType("longtext");
+
+ b.Property("Enabled")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("Gateway")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("GatewayCustomerId")
+ .HasColumnType("longtext");
+
+ b.Property("GatewaySubscriptionId")
+ .HasColumnType("longtext");
+
+ b.Property("Name")
+ .HasColumnType("longtext");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UseEvents")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Provider", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Settings")
+ .HasColumnType("longtext");
+
+ b.HasKey("Id");
+
+ b.HasIndex("OrganizationId");
+
+ b.HasIndex("ProviderId");
+
+ b.ToTable("ProviderOrganization", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Email")
+ .HasColumnType("longtext");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("Permissions")
+ .HasColumnType("longtext");
+
+ b.Property("ProviderId")
+ .HasColumnType("char(36)");
+
+ b.Property("RevisionDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Status")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("Type")
+ .HasColumnType("tinyint unsigned");
+
+ b.Property("UserId")
+ .HasColumnType("char(36)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ProviderId");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("ProviderUser", (string)null);
+ });
+
+ modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("char(36)");
+
+ b.Property("AccessCode")
+ .HasMaxLength(25)
+ .HasColumnType("varchar(25)");
+
+ b.Property("Approved")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("AuthenticationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreationDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Key")
+ .HasColumnType("longtext");
+
+ b.Property("MasterPasswordHash")
+ .HasColumnType("longtext");
+
+ b.Property("OrganizationId")
+ .HasColumnType("char(36)");
+
+ b.Property("PublicKey")
+ .HasColumnType("longtext");
+
+ b.Property