Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
37be83f
Add bulk auto-confirm functionality for organization users
JaredScar Apr 21, 2026
afff2ec
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar Apr 21, 2026
690c164
Refactor bulk auto-confirm functionality for organization users
JaredScar Apr 23, 2026
de7fe5b
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar Apr 24, 2026
8355124
Implement automatic user confirmation policy check in OrganizationUse…
JaredScar Apr 24, 2026
17be063
Implement bulk automatic user confirmation feature
JaredScar Apr 24, 2026
dae54ed
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar Apr 28, 2026
af1c50b
Refactor organization user confirmation process
JaredScar Apr 28, 2026
297bfff
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar Apr 29, 2026
aeed94b
Refactor BulkAutomaticallyConfirmOrganizationUsers functionality and …
JaredScar Apr 29, 2026
b655c4f
Add unit tests for BulkAutomaticallyConfirmOrganizationUsersValidator
JaredScar Apr 29, 2026
eb0539f
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar Apr 30, 2026
71f1d89
Refactor OrganizationUserRepository and update stored procedures
JaredScar Apr 30, 2026
b589881
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar May 7, 2026
30dec86
refactor: streamline bulk auto-confirmation process for organization …
JaredScar May 7, 2026
c61cf94
refactor: enhance auto-confirmation user validation process
JaredScar May 7, 2026
25079b0
refactor: simplify organization user auto-confirmation logic
JaredScar May 7, 2026
e6e85e4
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar May 7, 2026
9151b11
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar May 11, 2026
70ee3b1
Enhance unit tests by adding necessary imports for OrganizationUsersC…
JaredScar May 11, 2026
4266f26
Refactor OrganizationUsersController to filter pending users by UserI…
JaredScar May 11, 2026
3592508
Add import for DeleteClaimedAccount in BulkAutomaticallyConfirmOrgani…
JaredScar May 11, 2026
ac7221e
Enhance unit tests by adding necessary imports for OrganizationInvite…
JaredScar May 11, 2026
846eb8b
Update test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs
JaredScar May 11, 2026
688b405
Enhance unit tests by adding necessary imports for OrganizationInvite…
JaredScar May 11, 2026
0f48894
Enhance AutomaticallyConfirmOrganizationUser functionality by adding …
JaredScar May 11, 2026
f58e997
Update src/Infrastructure.EntityFramework/AdminConsole/Repositories/O…
JaredScar May 11, 2026
bbcd145
Enhance unit tests by adding necessary imports for OrganizationInvite…
JaredScar May 11, 2026
75b802d
Merge branch 'ac/pm-33919-automatically-confirm-pending-users-on-admi…
JaredScar May 11, 2026
b61b2c8
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar May 11, 2026
8f23199
Update test/Api.IntegrationTest/AdminConsole/Controllers/Organization…
JaredScar May 11, 2026
c2f71dd
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar May 12, 2026
6a50f29
Fix linting errors
JaredScar May 12, 2026
d8e5e4c
Fixes failing tests
JaredScar May 12, 2026
98d9f82
Add missing using directives in integration test files
JaredScar May 12, 2026
8118341
Implement user authorization check in AutomaticallyConfirmOrganizatio…
JaredScar May 12, 2026
a4f51af
Enhance BulkAutomaticallyConfirmOrganizationUsers tests by adding Org…
JaredScar May 12, 2026
6ecdb85
Add feature flag and organization ability setup in integration tests
JaredScar May 12, 2026
a25ad49
Update OrganizationUsersControllerPutResetPasswordTests to expect OK …
JaredScar May 12, 2026
4b0ebe0
Refactor user confirmation logic and enhance validation in BulkAutoma…
JaredScar May 12, 2026
e74d4cf
Update AutomaticallyConfirmOrganizationUserCommandTests to use Standa…
JaredScar May 12, 2026
6e3be1c
Refactor BulkAutomaticallyConfirmOrganizationUsers to return structur…
JaredScar May 12, 2026
7e36403
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar May 13, 2026
0677c00
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar May 14, 2026
f8ff29c
Add InjectOrganizationAttribute and OrganizationModelBinder for autom…
JaredScar May 14, 2026
a5a593a
Refactor InjectOrganizationAttribute and update OrganizationUsersCont…
JaredScar May 14, 2026
a40599e
Refactor BulkAutomaticallyConfirmOrganizationUsersCommand and related…
JaredScar May 14, 2026
ccce932
Update BulkAutomaticallyConfirmOrganizationUsersCommandTests and SQL …
JaredScar May 14, 2026
9072848
Enhance BulkAutomaticallyConfirmOrganizationUsers functionality and S…
JaredScar May 14, 2026
dd52dda
Refactor BulkAutomaticallyConfirmOrganizationUsers components and SQL…
JaredScar May 18, 2026
1c7f506
Merge branch 'main' of https://github.com/bitwarden/server into ac/pm…
JaredScar May 18, 2026
63565ae
feat(admin-console): Add InjectOrganizationAttribute and Organization…
JaredScar May 18, 2026
75ee8dc
feat(admin-console): Add bulk confirmation and pending auto-confirmat…
JaredScar May 18, 2026
8953698
refactor(admin-console): Change parameter type for ConfirmManyOrganiz…
JaredScar May 19, 2026
6e0458f
feat(admin-console): Introduce BindOrganizationAttribute and Organiza…
JaredScar May 19, 2026
f0942db
feat(admin-console): Update GetResetPasswordDetails to use BindOrgani…
JaredScar May 19, 2026
8af51c2
Add BindOrganizationAttribute and OrganizationModelBinder for organiz…
JaredScar May 19, 2026
bef2df2
Fix GetResetPasswordDetails method to use organization from parameter…
JaredScar May 20, 2026
fcded9a
fix(admin-console): Correct organization ID check in GetResetPassword…
JaredScar May 20, 2026
0452092
Merge branch 'main' into ac/pm-33919-db-changes
JaredScar May 21, 2026
7eaa3e1
Remove OrganizationUser_ReadByOrganizationIdStatus stored procedure a…
JaredScar May 21, 2026
e742d6a
Add integration tests for ConfirmManyOrganizationUsers and GetManyPen…
JaredScar May 21, 2026
dc6a36a
Merge branch 'main' into ac/pm-33919-inject-organization-attribute
JaredScar May 21, 2026
e255008
Refactor OrganizationUsersControllerTests to use bound organization i…
JaredScar May 21, 2026
71987d2
Refactor ConfirmManyOrganizationUsersAsync method to accept IReadOnly…
JaredScar May 21, 2026
f27ad23
Merge branch 'main' into ac/pm-33919-automatically-confirm-pending-us…
JaredScar May 21, 2026
e40f892
Implement OrganizationUser_UpdateStatusKey stored procedure and updat…
JaredScar May 22, 2026
9d78cb5
Merge branch 'main' into ac/pm-33919-db-changes
JaredScar May 22, 2026
0fdb651
Fix UTF-8 BOM issue in BindOrganizationAttribute.cs
JaredScar May 22, 2026
c1d20ec
Merge branch 'ac/pm-33919-inject-organization-attribute' of https://g…
JaredScar May 22, 2026
c703ebf
Merge branch 'ac/pm-33919-db-changes' of https://github.com/bitwarden…
JaredScar May 22, 2026
4d5f03c
Refactor OrganizationUser_UpdateStatusKey to OrganizationUser_UpdateM…
JaredScar May 22, 2026
3b3cbb6
Merge branch 'ac/pm-33919-db-changes' of https://github.com/bitwarden…
JaredScar May 22, 2026
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
60 changes: 60 additions & 0 deletions src/Api/AdminConsole/Attributes/BindOrganizationAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Bit.Api.AdminConsole.Authorization;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Bit.Api.AdminConsole.Attributes;

/// <summary>
/// Binds an <see cref="Organization"/> parameter by loading it from the database
/// using the <c>orgId</c> or <c>organizationId</c> route parameter.
/// </summary>
/// <remarks>
/// If the organization is not found, a <see cref="NotFoundException"/> is thrown.
/// </remarks>
/// <example>
/// <code><![CDATA[
/// [HttpPost("bulk-auto-confirm")]
/// public async Task<IResult> BulkAutomaticallyConfirmOrganizationUsersAsync(
/// [BindOrganization] Organization organization,
/// [FromBody] OrganizationUserBulkConfirmRequestModel model)
/// ]]></code>
/// </example>
[AttributeUsage(AttributeTargets.Parameter)]
public sealed class BindOrganizationAttribute() : ModelBinderAttribute(typeof(OrganizationModelBinder));

/// <summary>
/// Custom model binder that loads an <see cref="Organization"/> from the database
/// using the <c>orgId</c> or <c>organizationId</c> route parameter and binds it to the parameter.
/// </summary>
/// <remarks>
/// This binder is used via the <see cref="BindOrganizationAttribute"/>.
/// </remarks>
public class OrganizationModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
Guid orgId;
try
{
orgId = bindingContext.HttpContext.GetOrganizationId();
}
catch (InvalidOperationException)
{
throw new BadRequestException("Route parameter 'orgId' or 'organizationId' is missing or invalid.");
}

var repo = bindingContext.HttpContext.RequestServices
.GetRequiredService<IOrganizationRepository>();

var organization = await repo.GetByIdAsync(orgId);
if (organization is null)
{
throw new NotFoundException();
}

bindingContext.Result = ModelBindingResult.Success(organization);
}
}
63 changes: 51 additions & 12 deletions src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data;
using Bit.Core.AdminConsole.OrganizationFeatures.AccountRecovery;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
Expand Down Expand Up @@ -72,11 +73,13 @@ public class OrganizationUsersController : BaseAdminConsoleController
private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IGetPendingAutoConfirmUsersQuery _getPendingAutoConfirmUsersQuery;
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
private readonly IBulkResendOrganizationInvitesCommand _bulkResendOrganizationInvitesCommand;
private readonly IAutomaticallyConfirmOrganizationUserCommand _automaticallyConfirmOrganizationUserCommand;
private readonly IBulkAutomaticallyConfirmOrganizationUsersCommand _bulkAutomaticallyConfirmOrganizationUsersCommand;
private readonly V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand _revokeOrganizationUserCommandVNext;
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
Expand Down Expand Up @@ -117,9 +120,11 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor
IAdminRecoverAccountCommand adminRecoverAccountCommand,
AccountRecoveryV2.IAdminRecoverAccountCommand adminRecoverAccountCommandV2,
IAutomaticallyConfirmOrganizationUserCommand automaticallyConfirmOrganizationUserCommand,
IBulkAutomaticallyConfirmOrganizationUsersCommand bulkAutomaticallyConfirmOrganizationUsersCommand,
V2_RevokeOrganizationUserCommand.IRevokeOrganizationUserCommand revokeOrganizationUserCommandVNext,
ISelfRevokeOrganizationUserCommand selfRevokeOrganizationUserCommand,
IUpdateUserResetPasswordEnrollmentCommand updateUserResetPasswordEnrollmentCommand)
IUpdateUserResetPasswordEnrollmentCommand updateUserResetPasswordEnrollmentCommand,
IGetPendingAutoConfirmUsersQuery getPendingAutoConfirmUsersQuery)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
Expand All @@ -140,11 +145,13 @@ public OrganizationUsersController(IOrganizationRepository organizationRepositor
_deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand;
_getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;
_policyRequirementQuery = policyRequirementQuery;
_getPendingAutoConfirmUsersQuery = getPendingAutoConfirmUsersQuery;
_featureService = featureService;
_pricingClient = pricingClient;
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
_bulkResendOrganizationInvitesCommand = bulkResendOrganizationInvitesCommand;
_automaticallyConfirmOrganizationUserCommand = automaticallyConfirmOrganizationUserCommand;
_bulkAutomaticallyConfirmOrganizationUsersCommand = bulkAutomaticallyConfirmOrganizationUsersCommand;
_revokeOrganizationUserCommandVNext = revokeOrganizationUserCommandVNext;
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
Expand Down Expand Up @@ -229,10 +236,11 @@ private ListResponseModel<OrganizationUserUserDetailsResponseModel> GetResultLis

[HttpGet("{id}/reset-password-details")]
[Authorize<ManageAccountRecoveryRequirement>]
public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(Guid orgId, Guid id)
public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPasswordDetails(Guid id,
[BindOrganization] Organization organization)
{
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
if (organizationUser is null || organizationUser.OrganizationId != orgId || organizationUser.UserId is null)
if (organizationUser is null || organizationUser.OrganizationId != organization.Id || organizationUser.UserId is null)
{
throw new NotFoundException();
}
Expand All @@ -245,14 +253,7 @@ public async Task<OrganizationUserResetPasswordDetailsResponseModel> GetResetPas
throw new NotFoundException();
}

// Retrieve Encrypted Private Key from organization
var org = await _organizationRepository.GetByIdAsync(orgId);
if (org == null)
{
throw new NotFoundException();
}

return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, org));
return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, organization));
}

[HttpPost("account-recovery-details")]
Expand Down Expand Up @@ -797,7 +798,6 @@ public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute]
{
return TypedResults.Unauthorized();
}

return Handle(await _automaticallyConfirmOrganizationUserCommand.AutomaticallyConfirmOrganizationUserAsync(
new AutomaticallyConfirmOrganizationUserRequest
{
Expand All @@ -809,6 +809,45 @@ public async Task<IResult> AutomaticallyConfirmOrganizationUserAsync([FromRoute]
}));
}

[HttpGet("pending-auto-confirm")]
[Authorize<ManageUsersRequirement>]
[RequireFeature(FeatureFlagKeys.BulkAutoConfirmOnLogin)]
public async Task<ListResponseModel<OrganizationUserPendingAutoConfirmResponseModel>> GetPendingAutoConfirmUsersAsync(Guid orgId)
{
var pendingUsers = await _getPendingAutoConfirmUsersQuery.GetPendingAutoConfirmUsersAsync(orgId);
return new ListResponseModel<OrganizationUserPendingAutoConfirmResponseModel>(
pendingUsers.Select(u => new OrganizationUserPendingAutoConfirmResponseModel(u)));
}

[HttpPost("bulk-auto-confirm")]
[Authorize<ManageUsersRequirement>]
[RequireFeature(FeatureFlagKeys.BulkAutoConfirmOnLogin)]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkAutomaticallyConfirmOrganizationUsersAsync(
[BindOrganization] Organization organization,
[FromBody] OrganizationUserBulkConfirmRequestModel model)
{
var request = new BulkAutomaticallyConfirmOrganizationUsersRequest
{
Organization = organization,
DefaultUserCollectionName = model.DefaultUserCollectionName,
UsersToConfirm = model.Keys
.Select(entry => new BulkAutoConfirmUserEntry
{
OrganizationUserId = entry.Id,
Key = entry.Key,
})
.ToList(),
};

var results = await _bulkAutomaticallyConfirmOrganizationUsersCommand.RunAsync(request);

return new ListResponseModel<OrganizationUserBulkResponseModel>(results
.Select(r => new OrganizationUserBulkResponseModel(r.Id,
r.Result.Match(
error => error.Message,
_ => string.Empty))));
}

private async Task RestoreOrRevokeUserAsync(
Guid orgId,
Guid id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
ο»Ώusing Bit.Core.Entities;
using Bit.Core.Models.Api;

namespace Bit.Api.AdminConsole.Models.Response.Organizations;

public class OrganizationUserPendingAutoConfirmResponseModel : ResponseModel
{
public OrganizationUserPendingAutoConfirmResponseModel(OrganizationUser organizationUser)
: base("OrganizationUserPendingAutoConfirmResponseModel")
{
Id = organizationUser.Id;
UserId = organizationUser.UserId
?? throw new InvalidOperationException($"OrganizationUser {organizationUser.Id} has no associated UserId.");
}

/// <summary>The OrganizationUser ID.</summary>
public Guid Id { get; set; }

/// <summary>The User ID.</summary>
public Guid UserId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,4 @@ public OrganizationUserBulkResponseModel(Guid id, string error)
public Guid Id { get; set; }
public string Error { get; set; }
}

Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ private async Task<AutomaticallyConfirmOrganizationUserValidationRequest> Retrie
{
return new AutomaticallyConfirmOrganizationUserValidationRequest
{
OrganizationUserId = request.OrganizationUserId,
OrganizationId = request.OrganizationId,
Key = request.Key,
DefaultUserCollectionName = request.DefaultUserCollectionName,
PerformedBy = request.PerformedBy,
OrganizationUserId = request.OrganizationUserId,
OrganizationId = request.OrganizationId,
OrganizationUser = await organizationUserRepository.GetByIdAsync(request.OrganizationUserId),
Organization = await organizationRepository.GetByIdAsync(request.OrganizationId)
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.AutoConfirmUser;

/// <summary>
/// Automatically Confirm User Command Request
/// Automatically Confirm User Command Request (single-user).
/// </summary>
public record AutomaticallyConfirmOrganizationUserRequest
{
Expand All @@ -17,13 +17,40 @@ public record AutomaticallyConfirmOrganizationUserRequest
}

/// <summary>
/// Automatically Confirm User Validation Request
/// Hydrated request passed to the validator. Carries the retrieved <see cref="OrganizationUser"/> and
/// <see cref="Organization"/> objects directly. <see cref="OrganizationUserId"/> and
/// <see cref="OrganizationId"/> mirror the IDs from those objects (set explicitly to support
/// test scenarios where the hydrated objects may be null).
/// </summary>
/// <remarks>
/// This is used to hold retrieved data and pass it to the validator
/// </remarks>
public record AutomaticallyConfirmOrganizationUserValidationRequest : AutomaticallyConfirmOrganizationUserRequest
public record AutomaticallyConfirmOrganizationUserValidationRequest
{
public OrganizationUser? OrganizationUser { get; set; }
public Organization? Organization { get; set; }
public required string Key { get; init; }
public required string DefaultUserCollectionName { get; init; }
public OrganizationUser? OrganizationUser { get; init; }
public Organization? Organization { get; init; }
public IActingUser? PerformedBy { get; init; }
public Guid OrganizationUserId { get; init; }
public Guid OrganizationId { get; init; }
}

/// <summary>
/// Per-user entry for a bulk auto-confirm operation, containing only the user-specific fields.
/// </summary>
public record BulkAutoConfirmUserEntry
{
public required Guid OrganizationUserId { get; init; }
public required string Key { get; init; }
}

/// <summary>
/// Top-level request for bulk automatic user confirmation.
/// Shared fields (organization, collection name) are specified once rather than
/// repeated on every per-user entry.
/// </summary>
public record BulkAutomaticallyConfirmOrganizationUsersRequest
{
public required Organization Organization { get; init; }
public Guid OrganizationId => Organization.Id;
public required string DefaultUserCollectionName { get; init; }
public required IReadOnlyList<BulkAutoConfirmUserEntry> UsersToConfirm { get; init; }
}
Loading
Loading