Skip to content

Conversation

@jrmccannon
Copy link
Contributor

@jrmccannon jrmccannon commented Nov 19, 2025

🎟️ Tracking

PM-18718

📔 Objective

This is an optimized refactor of the bulk revoke users action. It utilizes the v2 Command Result and Validation Result patterns.

⏰ 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

…m-user-command

# Conflicts:
#	src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
#	src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs
#	test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs
…. Added ApplyAsync method to execute multiple functions against CommandResult without an error stopping the workflow for side-effects.
…thods for chaining ValidationResult together. Added logic for user type.
…sers

# Conflicts:
#	src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
@github-actions
Copy link
Contributor

github-actions bot commented Nov 19, 2025

Logo
Checkmarx One – Scan Summary & Details79dae63b-5d55-4219-b4ed-dc1ae30d459b

Fixed Issues (1)

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

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 489

@codecov
Copy link

codecov bot commented Nov 19, 2025

Codecov Report

❌ Patch coverage is 90.64748% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.34%. Comparing base (b0f6b22) to head (6e050ea).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
...Console/Controllers/OrganizationUsersController.cs 72.72% 4 Missing and 2 partials ⚠️
...Console/Repositories/OrganizationUserRepository.cs 0.00% 5 Missing ⚠️
...rs/RevokeUser/v2/RevokeOrganizationUsersRequest.cs 87.50% 1 Missing ⚠️
.../RevokeUser/v2/RevokeOrganizationUsersValidator.cs 95.83% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6601      +/-   ##
==========================================
+ Coverage   53.41%   57.34%   +3.93%     
==========================================
  Files        1917     1921       +4     
  Lines       85467    85601     +134     
  Branches     7667     7677      +10     
==========================================
+ Hits        45651    49092    +3441     
+ Misses      38048    34669    -3379     
- Partials     1768     1840      +72     

☔ 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.

@jrmccannon jrmccannon marked this pull request as ready for review November 20, 2025 17:02
@jrmccannon jrmccannon requested a review from a team as a code owner November 20, 2025 17:02
@jrmccannon jrmccannon requested a review from r-tome November 20, 2025 17:02
@claude
Copy link
Contributor

claude bot commented Nov 20, 2025

Claude finished @jrmccannon's task —— View job


Code Review Complete

Executive Summary

This PR successfully refactors the bulk revoke users functionality using v2 Command/Validation Result patterns. The implementation demonstrates excellent code quality with proper validation, comprehensive test coverage, and safe feature-flag deployment. However, there is one CRITICAL bug that must be fixed before merging.


Critical Issues ⚠️

1. CRITICAL: Provider Users Cannot Revoke Owners

Location: src/Api/AdminConsole/Controllers/OrganizationUsersController.cs:652

Problem:
The controller only checks OrganizationOwner(orgId) when creating the StandardUser:

new StandardUser(currentUserId.Value, await _currentContext.OrganizationOwner(orgId))

For provider users, this returns false, causing IsOrganizationOwnerOrProvider = false. The validator then incorrectly blocks the operation:

// RevokeOrganizationUsersValidator.cs:32-33
{ Type: OrganizationUserType.Owner } when !request.PerformedBy.IsOrganizationOwnerOrProvider =>
    Invalid(x, new OnlyOwnersCanRevokeOwners()),

Impact:

  • Providers cannot revoke organization owners (broken functionality)
  • Integration test BulkRevoke_ProviderRevokesOwner_ReturnsOk likely fails
  • Authorization layer (ManageUsersRequirement) correctly allows providers, but validation layer blocks them

Fix Required:

var isOwnerOrProvider = await _currentContext.OrganizationOwner(orgId) || 
                        await _currentContext.ProviderUserForOrgAsync(orgId);
new StandardUser(currentUserId.Value, isOwnerOrProvider)

Note: This same issue exists in v1 implementation (lines 92, 112), so it may have been missed in testing. Still must be fixed for v2.


Security Review ✅

Overall Security Status: SECURE (once Issue #1 is fixed)

Validated Security Controls:

  • Last Owner Protection: Correctly implemented via HasConfirmedOwnersExceptAsync (validator line 16-18, 30-31)
  • Self-Revocation Prevention: Properly blocks users from revoking themselves (validator line 24-27)
  • Owner Permissions: Enforces only owners can revoke owners (validator line 32-33) - blocked by Issue Identity Server  #1
  • Authorization: ManageUsersRequirement properly handles owners, admins, custom users, and providers
  • Audit Trail: All revocations logged via LogOrganizationUserEventsAsync
  • Input Validation: MinLength(1) prevents empty bulk operations
  • Zero-Knowledge Principles: No vault data exposed
  • No Sensitive Data in Logs: Error messages contain no PII or sensitive information

Code Quality & Architecture ✅

Strengths:

  • Excellent v2 Pattern Usage: Proper use of ValidationResult<T>, CommandResult<T>, and Error records
  • Clean Separation of Concerns: Validator handles business rules, command orchestrates operations
  • Immutable Data Structures: Converted to records per reviewer feedback (commit 69a5fde5c)
  • Proper Error Handling: Push notification failures logged but don't break operations
  • Performance Optimizations: Uses bulk operations (GetManyAsync, RevokeManyByIdAsync)
  • Feature Flag Safety: Proper fallback to v1 implementation

Breaking Changes from v1:

  1. Partial Success Behavior: v1 is all-or-nothing (throws on first error), v2 processes valid users and returns errors for invalid ones
  2. Error Granularity: v2 returns specific per-user errors vs v1's generic errors

These breaking changes should be documented in release notes.


Test Coverage Analysis

Unit Tests: ✅ EXCELLENT

Location: test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/

Comprehensive coverage including:

  • ✅ Valid users → success
  • ✅ Already revoked → error
  • ✅ Self-revocation → error
  • ✅ Non-owner revoking owner → error
  • ✅ Last owner → error (properly tested)
  • ✅ System users (SCIM) → proper handling
  • ✅ Mixed valid/invalid → partial success
  • ✅ Push notification failures → handled gracefully

Integration Tests: ⚠️ GOOD with GAPS

Location: test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUserControllerBulkRevokeTests.cs

Coverage includes:

  • ✅ Successful bulk revoke
  • ✅ Permission validation
  • ✅ Self-revocation blocked
  • ✅ Already revoked returns error
  • ✅ Admin cannot revoke owner
  • ✅ Mixed results (partial success)
  • ✅ Empty IDs validation
  • ✅ Invalid organization ID
  • ✅ Provider revokes owner (exists but likely FAILING due to Issue Identity Server  #1)

Missing Integration Tests:

  • Cannot revoke last confirmed owner - Critical business rule not integration-tested
  • ❌ SCIM user scenarios
  • ❌ Custom role with ManageUsers permission

Recommendation: Add integration test for last owner scenario to match unit test coverage.


Performance Review ✅

Optimizations:

  • Bulk Database Operations: Single queries via GetManyAsync and RevokeManyByIdAsync (improvement over v1's loop)
  • Batch Event Logging: Single call to LogOrganizationUserEventsAsync for all events
  • Proper Deduplication: Push notifications deduplicate user IDs (line 96-100)

Minor Consideration:

Push notifications sent sequentially (line 102-113). For very large bulk operations, parallel execution via Task.WhenAll could improve performance, but:

  • Sequential matches v1 behavior
  • Prevents overwhelming push service
  • Current implementation is acceptable

Additional Findings

Repository Changes ✅

Location: src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs:622-634

Clean formatting improvements to existing stored procedure call. No functional changes. Uses existing OrganizationUser_SetStatusForUsersByGuidIdArray procedure.

Error Definitions ✅

Location: src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeUser/v2/Errors.cs

Well-structured error records:

public record UserAlreadyRevoked() : BadRequestError("Already revoked.");
public record CannotRevokeYourself() : BadRequestError("You cannot revoke yourself.");
public record OnlyOwnersCanRevokeOwners() : BadRequestError("Only owners can revoke other owners.");
public record MustHaveConfirmedOwner() : BadRequestError("Organization must have at least one confirmed owner.");

All errors properly inherit from BadRequestError and will map to HTTP 400.

SCIM Integration ✅

Location: bitwarden_license/src/Scim/Controllers/v2/UsersController.cs and PatchUserCommand.cs

SCIM properly integrated with v2 patterns:

  • Uses SystemUser as PerformedBy (correctly bypasses self-revocation check)
  • Proper event logging with system user type
  • Test coverage exists

Recommendations

Before Merging (CRITICAL):

  1. FIX: Update controller to check both OrganizationOwner and ProviderUserForOrgAsync (Issue Identity Server  #1)
  2. VERIFY: Run integration test BulkRevoke_ProviderRevokesOwner_ReturnsOk - it likely fails
  3. ADD: Integration test for "cannot revoke last confirmed owner" scenario
  4. DOCUMENT: Add PR description note about breaking change in partial success behavior

Post-Merge (Optional):

  1. Consider adding integration tests for custom roles and additional edge cases
  2. Consider applying provider fix to v1 implementation for consistency
  3. Monitor performance of push notifications in production for very large bulk operations

Final Assessment

Code Quality: ⭐⭐⭐⭐⭐ Excellent
Test Coverage: ⭐⭐⭐⭐☆ Very Good (missing integration test for last owner)
Security: ⭐⭐⭐⭐⭐ Secure
Performance: ⭐⭐⭐⭐⭐ Optimized

Overall: Strong implementation that demonstrates excellent software engineering practices. The v2 pattern refactoring is well-executed with proper separation of concerns, comprehensive error handling, and good test coverage. However, the provider bug must be fixed before merging as it breaks existing functionality.


Tasks

  • Gather context and understand PR changes
  • Perform comprehensive code review using specialized agent
  • Analyze findings and prepare feedback
  • Post review results

public record UserAlreadyRevoked() : BadRequestError("Already revoked.");
public record CannotRevokeYourself() : BadRequestError("You cannot revoke yourself.");
public record OnlyOwnersCanRevokeOwners() : BadRequestError("Only owners can revoke other owners.");
public record MustHaveConfirmedOwner() : BadRequestError("Organization must have at least one confirmed owner.");
Copy link
Contributor

Choose a reason for hiding this comment

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

💭 The MustHaveConfirmedOwner error is defined but never used in the codebase. The v1 implementation checked for this condition with HasConfirmedOwnersExceptAsync, but the v2 implementation removed this check entirely.

Why this matters:
This could allow revoking the last owner of an organization, which would leave the organization in an unmanageable state. The v1 implementation prevented this at lines 62-66 and 84-87.

Recommendation:
Either implement the check in the validator and use this error, or remove the unused error definition. Based on the v1 implementation and business logic, the check should be implemented.


public class RevokeOrganizationUsersValidator : IRevokeOrganizationUserValidator
{
public ICollection<ValidationResult<OrganizationUser>> Validate(RevokeOrganizationUsersValidationRequest request) =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Critical: Missing "last owner" validation

The v1 implementation (RevokeUser/v1/RevokeOrganizationUserCommand.cs:62-66, 84-87) prevented revoking the last confirmed owner with HasConfirmedOwnersExceptAsync. This validation is completely absent in v2.

Security Impact:

  • Organizations can be left with zero confirmed owners
  • No one would be able to manage the organization
  • This is a breaking change from v1 behavior

Required Fix:
Add validation to ensure the organization will have at least one confirmed owner after the revocation:

// After line 9, inject IHasConfirmedOwnersExceptQuery
// In Validate method, before returning results:
var userIdsBeingRevoked = request.OrganizationUsersToRevoke.Select(u => u.Id);
if (!await hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
    request.OrganizationId, userIdsBeingRevoked, includeProvider: true))
{
    // Return error for all users being revoked
}

return;
}

await organizationUserRepository.RevokeManyByIdAsync(validUsers.Select(u => u.Id));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential performance issue with batched operations

The v1 implementation called RevokeAsync individually for each user (line 117 in v1), but v2 uses RevokeManyByIdAsync for all valid users at once. While this is generally more efficient, it creates a problem:

Issue:
If the database operation fails midway through a batch update, some users may be revoked while others aren't, but all would be marked as success in the results since validation passed.

The v1 approach:

  • Revoked users one at a time in a try-catch
  • Could partially succeed and return mixed results
  • Each user's success/failure was independent

Recommendation:
Consider whether the all-or-nothing batch approach is appropriate, or if partial success handling is needed. If partial failures are possible, wrap this in transaction handling or add error handling that updates the results accordingly.

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 would not be marked as a success because the repository would throw the exception and the transaction would be rolled back. This would be an "all-or-nothing" batch and the user would get a 500 error.

));
}

private async Task<RevokeOrganizationUsersValidationRequest> CreateValidationRequestsAsync(
Copy link
Contributor

Choose a reason for hiding this comment

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

🎨 Method can be made static or simplified

The CreateValidationRequestsAsync method only uses injected dependencies to fetch data and doesn't use any instance state. Consider:

  1. Making the repository calls directly in RevokeUsersAsync to reduce indirection
  2. Or making this method static if you prefer the separation

This would improve code clarity and reduce unnecessary method complexity.

{
OrganizationId = orgId,
OrganizationUserIdsToRevoke = model.Ids.ToArray(),
PerformedBy = new StandardUser(currentUserId.Value, await _currentContext.OrganizationOwner(orgId))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Inconsistent provider handling between v1 and v2

The v2 validator checks IsOrganizationOwnerOrProvider (line 19 in RevokeOrganizationUsersValidator.cs), but when creating the StandardUser here, we only pass the owner status from OrganizationOwner(orgId), not provider status.

The issue:
StandardUser constructor likely only sets the owner flag, but the validator expects IsOrganizationOwnerOrProvider to include both owner AND provider status. This could break provider revocation scenarios.

Integration test coverage:
The test BulkRevoke_ProviderRevokesOwner_ReturnsOk (OrganizationUserControllerBulkRevokeTests.cs:319) tests this scenario and should catch this, but verify the StandardUser implementation includes provider logic.

Recommendation:
Verify that StandardUser and IActingUser.IsOrganizationOwnerOrProvider properly handle provider permissions, or explicitly check provider status here.

// Assert
Assert.Single(results);
Assert.True(results.First().IsValid);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Missing test coverage for critical "last owner" scenario

The validator tests are comprehensive for the rules that ARE implemented, but there's no test for preventing the revocation of the last confirmed owner.

Missing test cases:

  1. Attempting to revoke the only confirmed owner (should fail)
  2. Attempting to revoke all confirmed owners in a batch (should fail)
  3. Attempting to revoke all but one owner, where the remaining owner is not confirmed (should fail)
  4. Revoking some owners while leaving at least one confirmed owner (should succeed)

These tests should be added once the "last owner" validation is implemented in the validator (see my comment on RevokeOrganizationUsersValidator.cs:10).

var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);

Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Integration test for "last owner" scenario is missing

The integration tests cover many scenarios but don't test the critical case of attempting to revoke the last confirmed owner. This is a business rule that was enforced in v1 but appears to be missing in v2.

Recommended additional test:

[Fact]
public async Task BulkRevoke_LastConfirmedOwner_ReturnsError()
{
    // Arrange: Create org with exactly one confirmed owner
    // Act: Attempt to revoke that owner
    // Assert: Should return error "Organization must have at least one confirmed owner."
}

This test would currently pass if the feature flag is disabled (using v1), but would likely fail with v2 enabled, confirming the missing validation.

r-tome
r-tome previously approved these changes Dec 3, 2025
Copy link
Contributor

@r-tome r-tome left a comment

Choose a reason for hiding this comment

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

Looks good, just minor suggestions

@jrmccannon jrmccannon requested a review from r-tome December 3, 2025 20:43
Copy link
Contributor

@r-tome r-tome left a comment

Choose a reason for hiding this comment

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

🚀

@jrmccannon jrmccannon merged commit 2f89376 into main Dec 5, 2025
48 checks passed
@jrmccannon jrmccannon deleted the jmccannon/ac/pm-18718-refactor-revoke-bulk-users branch December 5, 2025 17:19
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.

3 participants