Skip to content

Conversation

@r-tome
Copy link
Contributor

@r-tome r-tome commented Dec 4, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-21411

📔 Objective

Introduce IHasPremiumAccessQuery to centralize premium-access checks and make the distinction clearer between having a personal premium subscription (User.Premium) and actually having access to premium features (personal subscription or org membership).

This new query uses a new database view (UserPremiumAccessView) and a new stored procedure (User_ReadPremiumAccessByIds) to efficiently check premium status in bulk.

The implementation is gated behind the PremiumAccessQuery feature flag.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

@codecov
Copy link

codecov bot commented Dec 4, 2025

Codecov Report

❌ Patch coverage is 84.02367% with 27 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.52%. Comparing base (2e0a416) to head (6a783ff).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...rFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs 86.95% 8 Missing and 4 partials ⚠️
...e/Billing/Premium/Queries/HasPremiumAccessQuery.cs 65.38% 6 Missing and 3 partials ⚠️
src/Core/Services/Implementations/UserService.cs 14.28% 4 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6688      +/-   ##
==========================================
+ Coverage   53.64%   57.52%   +3.88%     
==========================================
  Files        1926     1926              
  Lines       85711    85855     +144     
  Branches     7686     7707      +21     
==========================================
+ Hits        45978    49390    +3412     
+ Misses      37961    34611    -3350     
- Partials     1772     1854      +82     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 4, 2025

Logo
Checkmarx One – Scan Summary & Details5a7a5eae-af30-4ab2-bf88-d635774aae1e

New Issues (2)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1519
detailsMethod at line 1519 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: dMGF5qNfAN72zlvQcA1MgbhHv%2Fc%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1395
detailsMethod at line 1395 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: iOCFr11iI9znjDnv46yLfiS4aDY%3D
Attack Vector
Fixed Issues (2)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/AdminConsole/Public/Controllers/MembersController.cs: 207
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 300

@eliykat eliykat self-requested a review December 5, 2025 00:36
Copy link
Member

@eliykat eliykat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work. I just noticed that User has a PremiumExpirationDate. Do we need to check if premium has expired? Or is the premium column automatically updated when the expiration date passes?

Comment on lines 34 to 40
/// <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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently unused, I recommend removing it if we don't have a clear use case for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will eventually replace

public async Task<bool> HasPremiumFromOrganization(ITwoFactorProvidersUser user)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be wired up in that method though? It's not called from anywhere at the moment (as far as I can see).

@r-tome r-tome changed the base branch from main to auth/remove-2fa-user-from-premium-methods December 5, 2025 15:00
…improved premium access checks and user detail handling. Removed obsolete feature service dependency and enhanced test coverage for new functionality.
…rloaded CanAccessPremiumAsync method. Update related methods to streamline premium access checks using the User object directly. Enhance test coverage by removing obsolete tests and ensuring proper functionality with the new method signatures.
…rDetails and User classes to clarify its usage and limitations regarding personal and organizational premium access.
…arameter with Guid for user ID in CanAccessPremiumAsync methods. Update related methods and tests to streamline premium access checks and improve clarity in method signatures.
@r-tome r-tome changed the title Ac/pm 21411/refactor interface for premium status [PM-21411] Refactor interface for determining premium status and features Dec 5, 2025
… use 'PremiumAccessQuery' instead of 'PremiumAccessCacheCheck'. Adjust related XML documentation for clarity on premium access methods.
@eliykat eliykat self-requested a review December 5, 2025 21:11
Comment on lines 34 to 40
/// <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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it need to be wired up in that method though? It's not called from anywhere at the moment (as far as I can see).

}

// Has org premium if has premium access but not personal premium
return user.HasPremiumAccess && !user.Premium;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is subtly different to the current implementation in UserService.HasPremiumFromOrganization: that current logic will return false if the user is not a part of any orgs, but will not return false if they have both personal premium and premium from an org. That doesn't seem right either, but it's unclear what the intent is.

That said, I have no idea why we need this logic: it's only synced to clients but doesn't seem to be used there either. I would be interested to know if we could remove it.

Any ideas @amorask-bitwarden ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have updated the new implementation to match the logic in UserService.

HasPremiumFromOrganization is used through SyncController for billing logic.

@eliykat eliykat self-requested a review December 8, 2025 22:24
Base automatically changed from auth/remove-2fa-user-from-premium-methods to main December 8, 2025 22:54
@trmartin4 trmartin4 dismissed amorask-bitwarden’s stale review December 8, 2025 22:54

The base branch was changed.

eliykat
eliykat previously approved these changes Dec 9, 2025
Copy link
Member

@eliykat eliykat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Nice work, I think the new db query really simplifies things. Other teams can still provide their feedback. (EDIT: and should! This mostly ended up outside of AC's domain so don't take my word for it.)

var userEntity = user as User ?? await _userRepository.GetByIdAsync(userId.Value);
if (userEntity == null)
{
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should throw - there should be a valid User in the database.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! Updated.

…tatus

# Conflicts:
#	src/Core/Constants.cs
#	src/Core/Services/IUserService.cs
…g and improve test clarity

* Replaced fully qualified exception references with simplified ones.
* Refactored test setup to use individual user variables for better readability.
* Ensured assertions reflect the updated user variable structure.
…istent users

* Updated TwoFactorIsEnabledQuery to throw NotFoundException when a user is not found instead of returning false.
* Added a new unit test to verify that the NotFoundException is thrown when a user is not found while premium access query is enabled.
@r-tome
Copy link
Contributor Author

r-tome commented Dec 9, 2025

I just noticed that User has a PremiumExpirationDate. Do we need to check if premium has expired? Or is the premium column automatically updated when the expiration date passes?

@eliykat the Premium field is automatically set to false. For example in cloud instances its done via a Stripe event https://github.com/bitwarden/server/blob/main/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs

* Updated CanAccessPremium and HasPremiumFromOrganization methods to clarify usage with the new premium access query.
* Integrated IHasPremiumAccessQuery into UserService for improved premium access handling based on feature flag.
* Adjusted method documentation to reflect changes in premium access logic.
* Modified Obsolete attribute messages for GetManyWithCalculatedPremiumAsync and GetCalculatedPremiumAsync to indicate that callers should use the new methods when the 'PremiumAccessQuery' feature flag is enabled.
* Enhanced documentation to improve clarity regarding premium access handling.
…mium access methods

* Modified Obsolete attribute messages for GetManyWithCalculatedPremiumAsync and GetCalculatedPremiumAsync in IUserRepository to indicate these methods will be removed in a future version.
* Updated Obsolete attribute message for HasPremiumFromOrganization in IUserService to reflect the same deprecation notice.
* Consolidated user ID retrieval logic to avoid redundancy.
* Ensured consistent handling of user ID checks for premium access queries.
* Improved code readability by reducing duplicate code blocks.
@r-tome r-tome marked this pull request as ready for review December 9, 2025 15:11
@r-tome r-tome requested review from a team as code owners December 9, 2025 15:11
@claude
Copy link
Contributor

claude bot commented Dec 9, 2025

Claude finished @r-tome's task —— View job


Code Review Complete

Tasks

  • Analyze PR changes and context
  • Perform comprehensive code review
  • Post review feedback

Code Review Summary

Overall Assessment:APPROVE

This PR introduces a well-architected solution for centralizing premium access checks with proper feature gating, comprehensive test coverage, and backward compatibility. The implementation correctly distinguishes between personal premium subscriptions and actual premium feature access (personal or organizational).


Key Findings by Category

Security ✓ PASS

  • Zero-knowledge principles maintained - No vault data involved in premium checks
  • Proper authorization context - Correctly validates organization membership and enabled status
  • No sensitive data exposure - Only checks boolean premium status flags
  • Query protection - Uses parameterized stored procedures (Dapper) and EF eager loading

Correctness ✓ PASS

  • Logic is sound: HasPremiumAccess = PersonalPremium OR OrganizationPremium
  • Proper org validation: Checks both UsersGetPremium = 1 AND Enabled = 1
  • Null handling: Throws NotFoundException for missing users (appropriate behavior)
  • Optimization: TwoFactorIsEnabledQuery only checks premium for users with premium-only providers
  • Duplicate prevention: Uses Distinct() on user IDs before bulk queries

Database Changes ✓ PASS

View (UserPremiumAccessView):

  • Correctly uses correlated subquery to check org membership
  • Properly casts result to BIT type
  • Read-only operation, no data modification risk

Stored Procedure (User_ReadPremiumAccessByIds):

  • Simple SELECT from view with table-valued parameter
  • Uses SET NOCOUNT ON (good practice)
  • Idempotent with CREATE OR ALTER

Index Update:

  • Added UsersGetPremium to IX_Organization_Enabled INCLUDE clause
  • Supports view's WHERE clause filtering
  • Migration script properly handles existing index

Migration Script: util/Migrator/DbScripts/2025-12-09_00_UserPremiumAccessView.sql

  • ✓ Dated 2025-12-09 (matches current date)
  • ✓ Idempotent operations

Feature Flag Implementation ✓ PASS

  • Flag: FeatureFlagKeys.PremiumAccessQuery = "pm-21411-premium-access-query"
  • Properly gated in:
    • TwoFactorIsEnabledQuery (3 methods)
    • UserService.CanAccessPremium()
    • UserService.HasPremiumFromOrganization()
  • Backward compatibility: Falls back to existing methods when flag disabled
  • Consistent usage: All call sites properly check flag before using new query

Test Coverage ✓ EXCELLENT

Unit Tests:

  • HasPremiumAccessQueryTests - 14 test cases covering all scenarios
  • TwoFactorIsEnabledQueryTests - 30+ test cases with/without feature flag
  • Tests verify optimization (premium only checked when needed)
  • Edge cases covered (null users, empty lists, mixed premium sources)

Integration Tests:

  • UserRepositoryTests - 8 new integration tests
  • Tests both Dapper and EF implementations
  • Validates disabled organizations don't grant premium
  • Tests bulk queries with multiple users and mixed premium sources

Code Quality ✓ PASS

  • Clear XML documentation on all public APIs
  • Consistent naming conventions
  • Proper dependency injection
  • Good separation of concerns (query in Billing.Premium.Queries namespace)
  • Obsolete methods include clear migration guidance

Breaking Changes ✓ NONE

  • All existing APIs maintained
  • Old methods marked [Obsolete] with migration path:
    • IUserRepository.GetManyWithCalculatedPremiumAsync() → Use GetPremiumAccessByIdsAsync()
    • IUserRepository.GetCalculatedPremiumAsync() → Use GetPremiumAccessAsync()
    • IUserService.HasPremiumFromOrganization() → Use IHasPremiumAccessQuery.HasPremiumFromOrganizationAsync()

Observations & Suggestions

🎨 SUGGESTED: Consider OrganizationUser index optimization

Location: src/Sql/dbo/Views/UserPremiumAccessView.sql:6-13

The view uses a correlated subquery on OrganizationUser:

WHERE OU.[UserId] = U.[Id]
    AND O.[UsersGetPremium] = 1
    AND O.[Enabled] = 1

Consider adding a composite index on OrganizationUser(UserId, OrganizationId) if one doesn't exist. While the Organization index was updated, the OrganizationUser table may benefit from an index on UserId for this lookup pattern.

Impact: Could improve query performance when checking premium access for many users.

🎨 SUGGESTED: EF implementation could use raw SQL query

Location: src/Infrastructure.EntityFramework/Repositories/UserRepository.cs:285-299

The EF implementation uses Include/ThenInclude which loads all organization data into memory:

var users = await dbContext.Users
    .Where(x => ids.Contains(x.Id))
    .Include(u => u.OrganizationUsers)
        .ThenInclude(ou => ou.Organization)
    .ToListAsync();

Consider using a raw SQL query to directly query the UserPremiumAccessView (matching the Dapper implementation) for better performance and consistency.

Current approach works correctly but loads more data than necessary.

❓ QUESTION: Migration script date

Location: util/Migrator/DbScripts/2025-12-09_00_UserPremiumAccessView.sql

The migration script is dated 2025-12-09. Is this intentional, or should it match today's actual date? The commit history shows it was renamed from an earlier date per commit 6a783ffed.

❓ QUESTION: Obsolete method removal timeline

Location: Multiple files with [Obsolete] attributes

The PR marks several methods as obsolete with clear migration guidance:

  • GetManyWithCalculatedPremiumAsync
  • GetCalculatedPremiumAsync
  • HasPremiumFromOrganization

Is there a follow-up ticket to remove these methods after the feature flag is enabled and stable? This would complete the refactoring.

💭 OBSERVATION: NotFoundException behavior in bulk query

Location: src/Core/Billing/Premium/Queries/HasPremiumAccessQuery.cs:31-34

if (usersWithPremium.Count() != distinctUserIds.Count)
{
    throw new NotFoundException();
}

This throws NotFoundException if ANY user in the batch is missing, but doesn't indicate WHICH user(s) are missing. This is probably acceptable for the use case, but could make debugging harder.

Current behavior is consistent with single-user method, so this is just an observation.

♻️ DEBT: TODO comment in production code

Location: src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs:224

// TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into.

This TODO appears in both the old and new code paths. Consider creating/linking the Jira ticket PM-21210 or removing if not actionable.


Architecture & Design ✓ EXCELLENT

The PR demonstrates excellent architectural decisions:

  1. Clear separation of concerns - Premium logic moved to Billing.Premium.Queries
  2. Interface-based design - IHasPremiumAccessQuery enables testability
  3. Feature flag pattern - Safe rollout with backward compatibility
  4. Database view abstraction - Encapsulates complex join logic
  5. Optimization strategy - Bulk queries and selective premium checks
  6. Clear naming - HasPremiumAccess vs PersonalPremium vs OrganizationPremium

Statistics

  • Files changed: 20
  • Additions: 1,163 lines
  • Deletions: 17 lines
  • New tests: ~50+ test cases
  • Integration tests: 8 new tests
  • Code coverage: 84% on new code (per Codecov)

Recommendation

APPROVE - This PR is ready to merge pending any discussion on the suggestions above.

The implementation is solid with:

  • No security concerns
  • No correctness issues
  • Comprehensive test coverage
  • Proper feature flagging
  • Clear migration path
  • Good documentation

The suggestions are optimizations that could be addressed in follow-up work if needed.


Copy link
Contributor

@cyprain-okeke cyprain-okeke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice Work, I just have some few clarifcations

@r-tome r-tome requested a review from cyprain-okeke December 10, 2025 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants