mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[AC-2328] Add a Bulk OrganizationUsersController.GetResetPasswordDetails endpoint (#4079)
* Add new stored procedure for reading reset password details for multiple organization user IDs * Add method IOrganizationUserRepository.GetManyResetPasswordDetailsByOrganizationUserAsync * Add new API endpoint for getting reset password details for multiple organization users * Add unit tests for bulk OrganizationUsersController.GetResetPasswordDetails * Add alias to sql query result column * Add constructor for automatic mapping * Fix http method type for endpoint * dotnet format * Simplify the constructor in the OrganizationUserResetPasswordDetails * Refactor stored procedure and repository method names for retrieving account recovery details * Add integration tests for GetManyAccountRecoveryDetailsByOrganizationUserAsync * Lock endpoint behind BulkDeviceApproval feature flag * Update feature flag key value
This commit is contained in:
@ -20,6 +20,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -186,6 +187,20 @@ public class OrganizationUsersController : Controller
|
||||
return new OrganizationUserResetPasswordDetailsResponseModel(new OrganizationUserResetPasswordDetails(organizationUser, user, org));
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.BulkDeviceApproval)]
|
||||
[HttpPost("account-recovery-details")]
|
||||
public async Task<ListResponseModel<OrganizationUserResetPasswordDetailsResponseModel>> GetAccountRecoveryDetails(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
// Make sure the calling user can reset passwords for this org
|
||||
if (!await _currentContext.ManageResetPassword(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var responses = await _organizationUserRepository.GetManyAccountRecoveryDetailsByOrganizationUserAsync(orgId, model.Ids);
|
||||
return new ListResponseModel<OrganizationUserResetPasswordDetailsResponseModel>(responses.Select(r => new OrganizationUserResetPasswordDetailsResponseModel(r)));
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task Invite(Guid orgId, [FromBody] OrganizationUserInviteRequestModel model)
|
||||
{
|
||||
|
@ -128,6 +128,7 @@ public class OrganizationUserResetPasswordDetailsResponseModel : ResponseModel
|
||||
throw new ArgumentNullException(nameof(orgUser));
|
||||
}
|
||||
|
||||
OrganizationUserId = orgUser.OrganizationUserId;
|
||||
Kdf = orgUser.Kdf;
|
||||
KdfIterations = orgUser.KdfIterations;
|
||||
KdfMemory = orgUser.KdfMemory;
|
||||
@ -136,6 +137,7 @@ public class OrganizationUserResetPasswordDetailsResponseModel : ResponseModel
|
||||
EncryptedPrivateKey = orgUser.EncryptedPrivateKey;
|
||||
}
|
||||
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
|
@ -6,6 +6,8 @@ namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
public class OrganizationUserResetPasswordDetails
|
||||
{
|
||||
public OrganizationUserResetPasswordDetails() { }
|
||||
|
||||
public OrganizationUserResetPasswordDetails(OrganizationUser orgUser, User user, Organization org)
|
||||
{
|
||||
if (orgUser == null)
|
||||
@ -23,6 +25,7 @@ public class OrganizationUserResetPasswordDetails
|
||||
throw new ArgumentNullException(nameof(org));
|
||||
}
|
||||
|
||||
OrganizationUserId = orgUser.Id;
|
||||
Kdf = user.Kdf;
|
||||
KdfIterations = user.KdfIterations;
|
||||
KdfMemory = user.KdfMemory;
|
||||
@ -30,6 +33,7 @@ public class OrganizationUserResetPasswordDetails
|
||||
ResetPasswordKey = orgUser.ResetPasswordKey;
|
||||
EncryptedPrivateKey = org.PrivateKey;
|
||||
}
|
||||
public Guid OrganizationUserId { get; set; }
|
||||
public KdfType Kdf { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
|
@ -43,6 +43,7 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task RestoreAsync(Guid id, OrganizationUserStatusType status);
|
||||
Task<IEnumerable<OrganizationUserPolicyDetails>> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType);
|
||||
Task<int> GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId);
|
||||
Task<IEnumerable<OrganizationUserResetPasswordDetails>> GetManyAccountRecoveryDetailsByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds);
|
||||
|
||||
/// <summary>
|
||||
/// Updates encrypted data for organization users during a key rotation
|
||||
|
@ -127,6 +127,7 @@ public static class FeatureFlagKeys
|
||||
public const string ExtensionRefresh = "extension-refresh";
|
||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||
public const string BulkDeviceApproval = "bulk-device-approval";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -523,6 +523,20 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationUserResetPasswordDetails>> GetManyAccountRecoveryDetailsByOrganizationUserAsync(
|
||||
Guid organizationId, IEnumerable<Guid> organizationUserIds)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<OrganizationUserResetPasswordDetails>(
|
||||
"[dbo].[OrganizationUser_ReadManyAccountRecoveryDetailsByOrganizationUserIds]",
|
||||
new { OrganizationId = organizationId, OrganizationUserIds = organizationUserIds.ToGuidIdArrayTVP() },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
|
||||
Guid userId, IEnumerable<OrganizationUser> resetPasswordKeys)
|
||||
|
@ -661,6 +661,26 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
return await GetCountFromQuery(query);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrganizationUserResetPasswordDetails>>
|
||||
GetManyAccountRecoveryDetailsByOrganizationUserAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
where organizationUserIds.Contains(ou.Id)
|
||||
join u in dbContext.Users
|
||||
on ou.UserId equals u.Id
|
||||
join o in dbContext.Organizations
|
||||
on ou.OrganizationId equals o.Id
|
||||
where ou.OrganizationId == organizationId
|
||||
select new { ou, u, o };
|
||||
var data = await query
|
||||
.Select(x => new OrganizationUserResetPasswordDetails(x.ou, x.u, x.o)).ToListAsync();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(
|
||||
Guid userId, IEnumerable<Core.Entities.OrganizationUser> resetPasswordKeys)
|
||||
|
@ -0,0 +1,24 @@
|
||||
CREATE PROCEDURE [dbo].[OrganizationUser_ReadManyAccountRecoveryDetailsByOrganizationUserIds]
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@OrganizationUserIds AS [dbo].[GuidIdArray] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
OU.[Id] AS OrganizationUserId,
|
||||
U.[Kdf],
|
||||
U.[KdfIterations],
|
||||
U.[KdfMemory],
|
||||
U.[KdfParallelism],
|
||||
OU.[ResetPasswordKey],
|
||||
O.[PrivateKey] AS EncryptedPrivateKey
|
||||
FROM @OrganizationUserIds AS OUIDs
|
||||
INNER JOIN [dbo].[OrganizationUser] AS OU
|
||||
ON OUIDs.[Id] = OU.[Id]
|
||||
INNER JOIN [dbo].[Organization] AS O
|
||||
ON OU.[OrganizationId] = O.[Id]
|
||||
INNER JOIN [dbo].[User] U
|
||||
ON U.[Id] = OU.[UserId]
|
||||
WHERE OU.[OrganizationId] = @OrganizationId
|
||||
END
|
Reference in New Issue
Block a user