Skip to content
Open
Show file tree
Hide file tree
Changes from 57 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
14f3e88
Adding auto confirm endpoint and initial command work.
jrmccannon Oct 23, 2025
4645222
Adding validator
jrmccannon Oct 23, 2025
6a29377
Finished command implementation.
jrmccannon Oct 23, 2025
89b8d59
Enabled the feature renomved used method. Enabled the policy in the tโ€ฆ
jrmccannon Oct 24, 2025
f60f0f5
Added extension functions to allow for railroad programming.
jrmccannon Oct 27, 2025
0ba770d
Removed guid from route template. Added xml docs
jrmccannon Oct 27, 2025
e5f07de
Merge branch 'refs/heads/main' into jmccannon/ac/pm-26636-auto-confirโ€ฆ
jrmccannon Oct 28, 2025
5527ca6
Added validation for command.
jrmccannon Oct 28, 2025
27ce4b8
Added default collection creation to command.
jrmccannon Oct 28, 2025
2069c9f
formatting.
jrmccannon Oct 28, 2025
cb81d29
Added additional error types and mapped to appropriate results.
jrmccannon Oct 29, 2025
24a39d8
Merge branch 'main' into jmccannon/ac/pm-26636-auto-confirm-user-command
jrmccannon Oct 29, 2025
a1b2bac
Added tests for auto confirm validator
jrmccannon Oct 30, 2025
7307454
Adding tests
jrmccannon Oct 30, 2025
1303b22
fixing file name
jrmccannon Oct 31, 2025
4bb8734
Cleaned up OrgUserController. Added integration tests.
jrmccannon Oct 31, 2025
68afb17
Consolidated CommandResult and validation result stuff into a v2 direโ€ฆ
jrmccannon Oct 31, 2025
aee2734
changing result to match handle method.
jrmccannon Oct 31, 2025
031c080
Moves validation thenasync method.
jrmccannon Oct 31, 2025
805eba9
Added brackets.
jrmccannon Nov 3, 2025
ce596b2
Updated XML comment
jrmccannon Nov 3, 2025
6e5188f
Adding idempotency comment.
jrmccannon Nov 3, 2025
d6a5813
Merge branch 'refs/heads/main' into jmccannon/ac/pm-26636-auto-confirโ€ฆ
jrmccannon Nov 3, 2025
9d47bf4
Fixed up merge problems. Fixed return types for handle.
jrmccannon Nov 3, 2025
929ac41
Renamed to ValidationRequest
jrmccannon Nov 3, 2025
f648953
I added some methods for CommandResult to cover some future use casesโ€ฆ
jrmccannon Nov 4, 2025
f800119
Fixed up logic around should create default colleciton. Added more meโ€ฆ
jrmccannon Nov 5, 2025
2e80a4d
Clearing nullable enable.
jrmccannon Nov 5, 2025
9b009b2
Fixed up validator tests.
jrmccannon Nov 5, 2025
e1eeaf9
Tests for auto confirm command
jrmccannon Nov 5, 2025
3af7ba4
Fixed up command result and AutoConfirmCommand.
jrmccannon Nov 5, 2025
cd1eca4
Merge branch 'main' into jmccannon/ac/pm-26636-auto-confirm-user-command
jrmccannon Nov 5, 2025
34a05f4
Removed some unused methods.
jrmccannon Nov 5, 2025
72fc706
Moved autoconfirm tests to their own class.
jrmccannon Nov 5, 2025
781d6e6
Moved some stuff around. Need to clean up creation of accepted org usโ€ฆ
jrmccannon Nov 6, 2025
e5ec8d7
Moved some more code around. Folded Key into accepted constructor. reโ€ฆ
jrmccannon Nov 6, 2025
eb5f98e
Merge branch 'main' into jmccannon/ac/pm-26636-auto-confirm-user-command
jrmccannon Nov 10, 2025
7fb8910
Merge branch 'refs/heads/main' into jmccannon/ac/pm-26636-auto-confirโ€ฆ
jrmccannon Nov 12, 2025
3ee2bf4
Clean up clean up everybody everywhere. Clean up clean up everybody dโ€ฆ
jrmccannon Nov 12, 2025
44a5813
Another quick one
jrmccannon Nov 12, 2025
a35c789
Removed aggregate Errors.cs
jrmccannon Nov 12, 2025
a9d4dc3
Cleaned up validator and fixed up tests.
jrmccannon Nov 12, 2025
290f391
Fixed auto confirm repo
jrmccannon Nov 12, 2025
f02f761
Cleaned up command tests.
jrmccannon Nov 12, 2025
fa18f3b
Unused method.
jrmccannon Nov 12, 2025
0487fb4
Restoring Bulk command back to what it was. deleted handle method forโ€ฆ
jrmccannon Nov 12, 2025
084849a
Remove unused method.
jrmccannon Nov 12, 2025
46b7bfd
removed unnecssary lines and comments
jrmccannon Nov 12, 2025
b4d5d50
fixed layout.
jrmccannon Nov 12, 2025
8dd84e2
Fixed test.
jrmccannon Nov 12, 2025
b91def1
fixed spelling mistake. removed unused import.
jrmccannon Nov 12, 2025
032b1f8
Merge branch 'main' into jmccannon/ac/pm-26636-auto-confirm-user-command
jrmccannon Nov 14, 2025
cdcff93
Update test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUโ€ฆ
jrmccannon Nov 14, 2025
7a5fd87
Merge remote-tracking branch 'origin/jmccannon/ac/pm-26636-auto-confiโ€ฆ
jrmccannon Nov 14, 2025
c301185
Ensuring collection is created before full sync. Cleaning up tests anโ€ฆ
jrmccannon Nov 14, 2025
4551891
Added org cleanup
jrmccannon Nov 14, 2025
6187cf0
Lowering to 5 to see if that helps the runner.
jrmccannon Nov 14, 2025
2b0b1ee
:shrug:
jrmccannon Nov 14, 2025
80872b8
Trying this
jrmccannon Nov 14, 2025
6b0af95
Maybe this time will be different.
jrmccannon Nov 14, 2025
64d3b1e
seeing if awaiting and checking independently will work in ci
jrmccannon Nov 14, 2025
3fb9810
I figured it out. Locally, it would be fast enough to all return NoCoโ€ฆ
jrmccannon Nov 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Api/AdminConsole/Controllers/BaseAdminConsoleController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
๏ปฟusing Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.AdminConsole.Utilities.v2.Results;
using Bit.Core.Models.Api;
using Microsoft.AspNetCore.Mvc;

namespace Bit.Api.AdminConsole.Controllers;

public abstract class BaseAdminConsoleController : Controller
{
protected static IResult Handle(CommandResult commandResult) =>
commandResult.Match<IResult>(
error => error switch
{
BadRequestError badRequest => TypedResults.BadRequest(new ErrorResponseModel(badRequest.Message)),
NotFoundError notFound => TypedResults.NotFound(new ErrorResponseModel(notFound.Message)),
InternalError internalError => TypedResults.Json(
new ErrorResponseModel(internalError.Message),
statusCode: StatusCodes.Status500InternalServerError),
_ => TypedResults.Json(
new ErrorResponseModel(error.Message),
statusCode: StatusCodes.Status500InternalServerError
)
},
_ => TypedResults.NoContent()
);
}
35 changes: 33 additions & 2 deletions src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,18 @@
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Utilities.v2;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Pricing;
Expand All @@ -43,7 +46,7 @@ namespace Bit.Api.AdminConsole.Controllers;

[Route("organizations/{orgId}/users")]
[Authorize("Application")]
public class OrganizationUsersController : Controller
public class OrganizationUsersController : BaseAdminConsoleController
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
Expand All @@ -68,6 +71,7 @@ public class OrganizationUsersController : Controller
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand;
Expand Down Expand Up @@ -101,7 +105,8 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor
IInitPendingOrganizationCommand initPendingOrganizationCommand,
IRevokeOrganizationUserCommand revokeOrganizationUserCommand,
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
IAdminRecoverAccountCommand adminRecoverAccountCommand)
IAdminRecoverAccountCommand adminRecoverAccountCommand,
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
Expand All @@ -126,6 +131,7 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor
_featureService = featureService;
_pricingClient = pricingClient;
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
_initPendingOrganizationCommand = initPendingOrganizationCommand;
Expand Down Expand Up @@ -738,6 +744,31 @@ public async Task PatchBulkEnableSecretsManagerAsync(Guid orgId,
await BulkEnableSecretsManagerAsync(orgId, model);
}

[HttpPost("{id}/auto-confirm")]
[Authorize<ManageUsersRequirement>]
[RequireFeature(FeatureFlagKeys.AutomaticConfirmUsers)]
public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute] Guid orgId,
[FromRoute] Guid id,
[FromBody] OrganizationUserConfirmRequestModel model)
{
var userId = _userService.GetProperUserId(User);

if (userId is null || userId.Value == Guid.Empty)
{
return TypedResults.Unauthorized();
}

return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync(
new AutomaticallyConfirmOrganizationUserRequest
{
OrganizationId = orgId,
OrganizationUserId = id,
Key = model.Key,
DefaultUserCollectionName = model.DefaultUserCollectionName,
PerformedBy = new StandardUser(userId.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.

โ“ Authorization question: The StandardUser is created with await _currentContext.OrganizationOwner(orgId), which checks if the current user is an owner. However, the [Authorize<ManageUsersRequirement>] attribute already ensures the user can manage users (which includes Admins, not just Owners).

Is it intentional that only Owners (not Admins) are allowed to auto-confirm users? If Admins should also be able to auto-confirm, this check may be too restrictive. The ManageUsersRequirement suggests both should be allowed.

Copy link
Contributor

Choose a reason for hiding this comment

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

โŒ Authorization inconsistency: The StandardUser is created with await _currentContext.OrganizationOwner(orgId), which only returns true for Owners, not Admins.

However, the endpoint has [Authorize<ManageUsersRequirement>] which allows both Owners and Admins to manage users.

This creates an inconsistency where:

  • Admins can pass the authorization check
  • But PerformedBy will have IsOwner = false for Admins
  • If any downstream logic depends on IsOwner, Admins won't be able to use this feature despite being authorized

Questions:

  1. Is this intentional? Should only Owners be able to auto-confirm users?
  2. If Admins should also be allowed, consider using _currentContext.OrganizationAdmin(orgId) or a different property name that doesn't imply Owner-only access.

Note: This same pattern exists in other endpoints, but for auto-confirm specifically, the validator doesn't seem to check the PerformedBy.IsOwner property, so this may currently work but is semantically confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

๐Ÿ’ญ Authorization semantic question: The StandardUser is created with await _currentContext.OrganizationOwner(orgId), which only returns true for Owners, not Admins.

However, the endpoint has [Authorize<ManageUsersRequirement>] which allows both Owners and Admins to manage users.

Questions:

  1. Is this intentional? Should only Owners be able to auto-confirm users?
  2. If Admins should also be allowed, should IsOwner be true for them, or is this property only for audit purposes?

The validator doesn't check PerformedBy.IsOwner, so this currently works but is semantically confusing. Consider either documenting this is intentional or using a different property name that doesn't imply Owner-only access.

}));
}

private async Task RestoreOrRevokeUserAsync(
Guid orgId,
Guid id,
Expand Down
1 change: 1 addition & 0 deletions src/Core/AdminConsole/Enums/EventType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public enum EventType : int
OrganizationUser_RejectedAuthRequest = 1514,
OrganizationUser_Deleted = 1515, // Both user and organization user data were deleted
OrganizationUser_Left = 1516, // User voluntarily left the organization
OrganizationUser_AutomaticallyConfirmed = 1517,

Organization_Updated = 1600,
Organization_PurgedVault = 1601,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
๏ปฟnamespace Bit.Core.AdminConsole.Models.Data.OrganizationUsers;

public record AcceptedOrganizationUserToConfirm
{
public required Guid OrganizationUserId { get; init; }
public required Guid UserId { get; init; }
public required string Key { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
๏ปฟusing Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.Extensions.Logging;
using OneOf.Types;
using CommandResult = Bit.Core.AdminConsole.Utilities.v2.Results.CommandResult;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;

public class AutomaticallyConfirmOrganizationUserCommand(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IAutomaticallyConfirmOrganizationUsersValidator validator,
IEventService eventService,
IMailService mailService,
IUserRepository userRepository,
IPushRegistrationService pushRegistrationService,
IDeviceRepository deviceRepository,
IPushNotificationService pushNotificationService,
IPolicyRequirementQuery policyRequirementQuery,
ICollectionRepository collectionRepository,
TimeProvider timeProvider,
ILogger<AutomaticallyConfirmOrganizationUserCommand> logger) : IAutomaticallyConfirmOrganizationUserCommand
{
public async Task<CommandResult> AutomaticallyConfirmOrganizationUserAsync(AutomaticallyConfirmOrganizationUserRequest request)
{
var validatorRequest = await RetrieveDataAsync(request);

var validatedData = await validator.ValidateAsync(validatorRequest);

return await validatedData.Match<Task<CommandResult>>(
error => Task.FromResult(new CommandResult(error)),
async _ =>
{
var userToConfirm = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = validatedData.Request.OrganizationUser!.Id,
UserId = validatedData.Request.OrganizationUser.UserId!.Value,
Key = validatedData.Request.Key
};

// This operation is idempotent. If false, the user is already confirmed and no additional side effects are required.
if (!await organizationUserRepository.ConfirmOrganizationUserAsync(userToConfirm))
{
return new None();
}

await CreateDefaultCollectionsAsync(validatedData.Request);
Copy link
Contributor

Choose a reason for hiding this comment

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

๐Ÿ‘ Excellent work addressing the previous feedback! Creating the default collection before the parallel tasks ensures it exists before the sync notification is sent to clients. This prevents a race condition where clients might sync before the collection is created.


await Task.WhenAll(
LogOrganizationUserConfirmedEventAsync(validatedData.Request),
SendConfirmedOrganizationUserEmailAsync(validatedData.Request),
SyncOrganizationKeysAsync(validatedData.Request)
);

return new None();
}
);
}

private async Task SyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
Copy link
Contributor

Choose a reason for hiding this comment

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

๐Ÿ’ญ Sequential operation question: This method enforces sequential execution: delete device registrations first, then push sync keys.

Question: Is there a dependency between these operations? For example:

  • Should the push notification only go to devices that haven't been unregistered?
  • Or is this just a logical grouping?

If there's no dependency, these could be parallelized with the other operations. If there is a dependency, a comment explaining why would be helpful for future maintainers.

Given the method name SyncOrganizationKeysAsync implies they're related, the sequential execution seems intentional, but documentation would help clarify.

{
await DeleteDeviceRegistrationAsync(request);
await PushSyncOrganizationKeysAsync(request);
Copy link
Contributor

Choose a reason for hiding this comment

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

โš ๏ธ Potential race condition: DeleteDeviceRegistrationAsync and PushSyncOrganizationKeysAsync are called sequentially here, but SyncOrganizationKeysAsync is executed in parallel with other tasks via Task.WhenAll (line 54-58).

If there's a dependency between deleting device registrations and pushing sync keys (e.g., the push notification should only go to devices that haven't been unregistered), the parallel execution could cause the push to go to already-deleted registrations.

Recommendation: Either:

  1. Document that there's no dependency and this is intentional
  2. Or extract this as a single sequential operation if ordering matters

}

private async Task CreateDefaultCollectionsAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
if (!await ShouldCreateDefaultCollectionAsync(request))
{
return;
}

await collectionRepository.CreateAsync(
new Collection
{
OrganizationId = request.Organization!.Id,
Name = request.DefaultUserCollectionName,
Type = CollectionType.DefaultUserCollection
},
groups: null,
[new CollectionAccessSelection
{
Id = request.OrganizationUser!.Id,
Manage = true
}]);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create default collection for user.");
}
}

/// <summary>
/// Determines whether a default collection should be created for an organization user during the confirmation process.
/// </summary>
/// <param name="request">
/// The validation request containing information about the user, organization, and collection settings.
/// </param>
/// <returns>The result is a boolean value indicating whether a default collection should be created.</returns>
private async Task<bool> ShouldCreateDefaultCollectionAsync(AutomaticallyConfirmOrganizationUserValidationRequest request) =>
!string.IsNullOrWhiteSpace(request.DefaultUserCollectionName)
&& (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value))
Copy link
Contributor

Choose a reason for hiding this comment

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

๐Ÿ“ Code clarity: The null-forgiving operators (!) here are safe due to prior validation, but a brief comment would help future maintainers understand why these are guaranteed to be non-null.

// Safe to use ! operators: validator ensures OrganizationUser and UserId are non-null
&& (await policyRequirementQuery.GetAsync<OrganizationDataOwnershipPolicyRequirement>(request.OrganizationUser!.UserId!.Value))

Copy link
Contributor

Choose a reason for hiding this comment

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

๐ŸŽจ Null-forgiving operator usage: The null-forgiving operators (!) here are safe because the validator guarantees these values are non-null. However, as mentioned in a previous review thread, this is actually less type-safe than using nullable reference types.

The compiler won't prevent you from accessing AsError or checking these properties without proper validation first, which could cause runtime errors. With nullable types, the compiler enforces null checks.

Non-blocking observation: This is a broader architectural consideration about the tradeoff between FP patterns (OneOf/Result types) and C#'s nullable reference types. The current approach works but requires discipline to ensure validation always precedes usage of these operators.

.RequiresDefaultCollectionOnConfirm(request.Organization!.Id);

private async Task PushSyncOrganizationKeysAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
await pushNotificationService.PushSyncOrgKeysAsync(request.OrganizationUser!.UserId!.Value);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to push organization keys.");
}
}

private async Task LogOrganizationUserConfirmedEventAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
await eventService.LogOrganizationUserEventAsync(request.OrganizationUser,
EventType.OrganizationUser_AutomaticallyConfirmed,
timeProvider.GetUtcNow().UtcDateTime);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to log OrganizationUser_AutomaticallyConfirmed event.");
}
}

private async Task SendConfirmedOrganizationUserEmailAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
Copy link
Contributor

Choose a reason for hiding this comment

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

โš ๏ธ Potential null reference: The user is fetched but could theoretically be null. While the validator ensures OrganizationUser.UserId is non-null, there's no guarantee the User entity exists in the database.

Suggested fix:

var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
if (user == null)
{
    logger.LogError("User not found when sending confirmation email.");
    return;
}

await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
    user.Email,
    request.OrganizationUser.AccessSecretsManager);

This is caught by the try-catch, but explicit null checking provides clearer error logging.

Copy link
Contributor

Choose a reason for hiding this comment

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

โš ๏ธ Potential null reference: The user is fetched but could theoretically be null. While the validator ensures OrganizationUser.UserId is non-null, there's no guarantee the User entity exists in the database.

Suggested fix:

var user = await userRepository.GetByIdAsync(request.OrganizationUser!.UserId!.Value);
if (user == null)
{
    logger.LogError("User {UserId} not found when sending confirmation email.", request.OrganizationUser.UserId.Value);
    return;
}

await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
    user.Email,
    request.OrganizationUser.AccessSecretsManager);

This is caught by the try-catch, but explicit null checking provides clearer error logging.


await mailService.SendOrganizationConfirmedEmailAsync(request.Organization!.Name,
user!.Email,
request.OrganizationUser.AccessSecretsManager);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to send OrganizationUserConfirmed.");
}
}

private async Task DeleteDeviceRegistrationAsync(AutomaticallyConfirmOrganizationUserValidationRequest request)
{
try
{
var devices = (await deviceRepository.GetManyByUserIdAsync(request.OrganizationUser!.UserId!.Value))
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => d.Id.ToString());

await pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices, request.Organization!.Id.ToString());
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to delete device registration.");
}
}

private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> RetrieveDataAsync(
AutomaticallyConfirmOrganizationUserRequest request)
{
return new AutomaticallyConfirmOrganizationUserValidationRequest
{
OrganizationUserId = request.OrganizationUserId,
OrganizationId = request.OrganizationId,
Key = request.Key,
DefaultUserCollectionName = request.DefaultUserCollectionName,
PerformedBy = request.PerformedBy,
OrganizationUser = await organizationUserRepository.GetByIdAsync(request.OrganizationUserId),
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
๏ปฟusing Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.Entities;

namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;

/// <summary>
/// Automatically Confirm User Command Request
/// </summary>
public record AutomaticallyConfirmOrganizationUserRequest
{
public required Guid OrganizationUserId { get; init; }
public required Guid OrganizationId { get; init; }
public required string Key { get; init; }
public required string DefaultUserCollectionName { get; init; }
public required IActingUser PerformedBy { get; init; }
}

/// <summary>
/// Automatically Confirm User Validation Request
/// </summary>
/// <remarks>
/// This is used to hold retrieved data and pass it to the validator
/// </remarks>
public record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest
{
public OrganizationUser? OrganizationUser { get; set; }
public Organization? Organization { get; set; }
}
Loading
Loading